{
  "generated_at": "2026-08-31 02:39:51",
  "generated_at_iso": "2026-08-31T02:39:51.794767",
  "repository": {
    "name": "revontulet-website",
    "branch": "main",
    "url": "https://github.com/zRenard/revontulet-website"
  },
  "commits": [
    {
      "hash": "d6e146052154f07baa7f3450ed88aa35be474c34",
      "short_hash": "d6e1460",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788143951,
      "message": "SPOR-650 - App - Modales : formulaires longs défilables sur mobile\n\nSur téléphone, une modale plus haute que l'écran (édition de course surtout,\nmais aussi gear / séance / …) débordait sans défiler — le bouton\n« Enregistrer » devenait inatteignable.\n\nLe comportement de .rv-modal--scroll (jusqu'ici sur une seule modale) passe\ndans la classe de base .rv-modal : display:flex; flex-direction:column;\nmax-height: calc(100dvh - 40px) (repli 100vh) + .rv-modal__body {\noverflow-y:auto; min-height:0 }. L'entête reste visible, le corps défile.\n@media (max-width:640px) : padding d'overlay 20 -> 12 px. CSS uniquement,\nrien dans References/design/.\n\nDoc : TECHNICAL.md, mindmap.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-650 - App - Modales : formulaires longs défilables sur mobile",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/css/app.css"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 22,
          "deletions": 0,
          "lines_changed": 22
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 14,
          "deletions": 4,
          "lines_changed": 18
        }
      ],
      "files_changed": 3,
      "insertions": 37,
      "deletions": 4,
      "tags": [
        "v1.4.2"
      ]
    },
    {
      "hash": "36df5802082cc82569d74e292a42892a35387f3b",
      "short_hash": "36df580",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788143194,
      "message": "SPOR-649 - Objectifs - Courses : exploiter l'heure de départ\n\ngoals.start_time (introduite par SPOR-648 pour la météo) sert désormais aussi\nà l'affichage et à la logique.\n\n- Domain\\Training\\RaceSchedule (pur, testé) : startInstant, preciseCountdown\n  (h/min si < 48 h), isOngoing (jour J + heure atteinte).\n- Http\\Training\\RaceCountdownLabel : factorise les 3 copies de\n  daysCountdownLabel() (supprimées) ; compte à rebours horaire sinon jours.\n- Ruban « En cours » (/races) et mise en avant « Jour J » (Dashboard) calés\n  sur RaceSchedule::isOngoing() au lieu de minuit.\n- timeLabel exposé par RacesController / RaceDetailPresenter /\n  DashboardController::nextRaces / EventRacePicker::forEvent ; rendu « · HH:MM »\n  après la date (races.php, races_detail.php, dashboard.php, event_detail.php).\n- i18n FR/EN : goals.hm_to_go, goals.minutes_to_go, races.at_time.\n- Doc : TECHNICAL.md, docs/objectifs.md, mindmap.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-649 - Objectifs - Courses : exploiter l'heure de départ",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/objectifs.md",
        "src/Domain/Training/RaceSchedule.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Events/EventRacePicker.php",
        "src/Http/Goals/GoalFormHelpers.php",
        "src/Http/Goals/RaceDetailPresenter.php",
        "src/Http/Goals/RacesController.php",
        "src/Http/Training/RaceCountdownLabel.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dashboard.php",
        "templates/event_detail.php",
        "templates/races.php",
        "templates/races_detail.php",
        "tests/Domain/Training/RaceScheduleTest.php"
      ],
      "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": "docs/objectifs.md",
          "insertions": 13,
          "deletions": 2,
          "lines_changed": 15
        },
        {
          "path": "src/Domain/Training/RaceSchedule.php",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 20,
          "deletions": 23,
          "lines_changed": 43
        },
        {
          "path": "src/Http/Events/EventRacePicker.php",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "src/Http/Goals/GoalFormHelpers.php",
          "insertions": 5,
          "deletions": 12,
          "lines_changed": 17
        },
        {
          "path": "src/Http/Goals/RaceDetailPresenter.php",
          "insertions": 9,
          "deletions": 15,
          "lines_changed": 24
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 8,
          "deletions": 2,
          "lines_changed": 10
        },
        {
          "path": "src/Http/Training/RaceCountdownLabel.php",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "templates/event_detail.php",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "templates/races.php",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "templates/races_detail.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "tests/Domain/Training/RaceScheduleTest.php",
          "insertions": 77,
          "deletions": 0,
          "lines_changed": 77
        }
      ],
      "files_changed": 17,
      "insertions": 304,
      "deletions": 65,
      "tags": [
        "v1.4.1"
      ]
    },
    {
      "hash": "abedc1e848c0a00e0a43afdb3df02f99c58bd3cd",
      "short_hash": "abedc1e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788135661,
      "message": "SPOR-648 - Objectifs - Courses : trace GPS + prédiction affinée (dénivelé & température)\n\nAttacher une trace GPS à une course (upload GPX/TCX ou parcours /routes) pour\nque la prédiction de temps tienne compte du dénivelé du parcours et de la\ntempérature prévue le jour J.\n\n- Migration 137 : goals.start_time + route_gpx (BLOB) + dérivés (distance,\n  D+/D-, départ lat/lon privées, polyline, profil d'altitude, source, modèle).\n- Domain pur & testé : RouteFileParser (GPX trk/rte + TCX, DOCTYPE/NONET/25Mo),\n  RouteEquivalentDistance (modèle 1), GradeAdjustedDistance (modèle 3 Minetti),\n  TemperaturePaceAdjustment (multiplier ≥ 1, plafond 12 %).\n- Http\\Goals\\RaceTraceStore (attach upload / parcours existant / remove).\n- Http\\Training\\RaceForecastAdjustment::resolve() : elevationFactor (run) +\n  multiplier température (course ≤ 8 j, dégradation gracieuse Open-Meteo).\n- RaceProjection::build() : distance × elevationFactor puis × température ×\n  blessure, avant RaceProjectionGap. WeatherProvider injecté dans Races/\n  RaceDetail/GoalsController.\n- Routes POST /races/{id}/route[/remove]. start_time + route_elevation_model\n  persistés par le chemin d'édition (comme injury_adjustment SPOR-647).\n- UI : carte Parcours sur /races/{id}, partial race_projection_flags.php\n  (icônes dénivelé/température), champs modale. i18n FR/EN.\n- Doc : TECHNICAL.md, docs/objectifs.md, mindmap.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-648 - Objectifs - Courses : trace GPS + prédiction affinée (dénivelé & température)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/137_add_race_gps_trace.sql",
        "docs/objectifs.md",
        "src/Domain/Import/RouteFileParser.php",
        "src/Domain/Training/GradeAdjustedDistance.php",
        "src/Domain/Training/RouteEquivalentDistance.php",
        "src/Domain/Training/TemperaturePaceAdjustment.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Goals/GoalFormHelpers.php",
        "src/Http/Goals/GoalsController.php",
        "src/Http/Goals/RaceDetailController.php",
        "src/Http/Goals/RaceDetailPresenter.php",
        "src/Http/Goals/RaceTraceStore.php",
        "src/Http/Goals/RacesController.php",
        "src/Http/Training/RaceForecastAdjustment.php",
        "src/Http/Training/RaceProjection.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dashboard.php",
        "templates/goals.php",
        "templates/partials/goal_form_fields.php",
        "templates/partials/race_projection_flags.php",
        "templates/races.php",
        "templates/races_detail.php",
        "tests/Domain/Import/RouteFileParserTest.php",
        "tests/Domain/Training/GradeAdjustedDistanceTest.php",
        "tests/Domain/Training/RouteEquivalentDistanceTest.php",
        "tests/Domain/Training/TemperaturePaceAdjustmentTest.php",
        "tests/fixtures/route_hilly.gpx",
        "tests/fixtures/route_planned.gpx"
      ],
      "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": "config/routes.php",
          "insertions": 5,
          "deletions": 3,
          "lines_changed": 8
        },
        {
          "path": "db/migrations/137_add_race_gps_trace.sql",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 31,
          "deletions": 1,
          "lines_changed": 32
        },
        {
          "path": "src/Domain/Import/RouteFileParser.php",
          "insertions": 198,
          "deletions": 0,
          "lines_changed": 198
        },
        {
          "path": "src/Domain/Training/GradeAdjustedDistance.php",
          "insertions": 71,
          "deletions": 0,
          "lines_changed": 71
        },
        {
          "path": "src/Domain/Training/RouteEquivalentDistance.php",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "src/Domain/Training/TemperaturePaceAdjustment.php",
          "insertions": 60,
          "deletions": 0,
          "lines_changed": 60
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 7,
          "deletions": 2,
          "lines_changed": 9
        },
        {
          "path": "src/Http/Goals/GoalFormHelpers.php",
          "insertions": 24,
          "deletions": 3,
          "lines_changed": 27
        },
        {
          "path": "src/Http/Goals/GoalsController.php",
          "insertions": 7,
          "deletions": 1,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Goals/RaceDetailController.php",
          "insertions": 16,
          "deletions": 3,
          "lines_changed": 19
        },
        {
          "path": "src/Http/Goals/RaceDetailPresenter.php",
          "insertions": 45,
          "deletions": 2,
          "lines_changed": 47
        },
        {
          "path": "src/Http/Goals/RaceTraceStore.php",
          "insertions": 185,
          "deletions": 0,
          "lines_changed": 185
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 124,
          "deletions": 6,
          "lines_changed": 130
        },
        {
          "path": "src/Http/Training/RaceForecastAdjustment.php",
          "insertions": 185,
          "deletions": 0,
          "lines_changed": 185
        },
        {
          "path": "src/Http/Training/RaceProjection.php",
          "insertions": 32,
          "deletions": 3,
          "lines_changed": 35
        },
        {
          "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/dashboard.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/goals.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/partials/goal_form_fields.php",
          "insertions": 21,
          "deletions": 1,
          "lines_changed": 22
        },
        {
          "path": "templates/partials/race_projection_flags.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "templates/races.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/races_detail.php",
          "insertions": 58,
          "deletions": 1,
          "lines_changed": 59
        },
        {
          "path": "tests/Domain/Import/RouteFileParserTest.php",
          "insertions": 103,
          "deletions": 0,
          "lines_changed": 103
        },
        {
          "path": "tests/Domain/Training/GradeAdjustedDistanceTest.php",
          "insertions": 62,
          "deletions": 0,
          "lines_changed": 62
        },
        {
          "path": "tests/Domain/Training/RouteEquivalentDistanceTest.php",
          "insertions": 49,
          "deletions": 0,
          "lines_changed": 49
        },
        {
          "path": "tests/Domain/Training/TemperaturePaceAdjustmentTest.php",
          "insertions": 49,
          "deletions": 0,
          "lines_changed": 49
        },
        {
          "path": "tests/fixtures/route_hilly.gpx",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "tests/fixtures/route_planned.gpx",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        }
      ],
      "files_changed": 32,
      "insertions": 1621,
      "deletions": 29,
      "tags": [
        "v1.4.0"
      ]
    },
    {
      "hash": "61baef2a0078973f5cb1780c35e08a942b467224",
      "short_hash": "61baef2",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788128891,
      "message": "SPOR-647 - Objectifs - Prédictions de course : ajustement selon une blessure active\n\nUne blessure en cours ralentit la prédiction Riegel/Cameron/Daniels au-delà\nde la dégradation déjà portée par la séance de référence, avec une icône et\nun opt-out par course + global (les deux ON par défaut).\n\n- Migration 136 : users.injury_prediction_adjustment + goals.injury_adjustment.\n- Domain\\Training\\InjuryPredictionAdjustment (pur, testé) : multiplier ≥ 1,\n  pénalité = min(10%, 6% × intensité/10 × poids évolution) ;\n  poids worsening 1.25 / stable 1.0 / improving 0.5.\n- Http\\Training\\InjuryAdjustmentContext::resolve() : 2 flags + blessure active\n  la plus lourde à une date (resolved_at > asOf) ; null sinon.\n- RaceProjection::build() : param optionnel, applique le multiplier avant\n  RaceProjectionGap ; câblé dans les 4 points de projection + vsPrediction\n  (asOf = date de la course pour les résultats passés).\n- UI : partial injury_projection_icon.php (icône activity + tooltip) sur\n  dashboard/races/races_detail/goals/session_detail ; case par course\n  (goal_form_fields, branches performance + text) ; case globale (Settings\n  onglet Analyse) ; POST /races/{id}/injury-adjustment (fiche course).\n- i18n FR/EN. Doc : TECHNICAL.md, docs/objectifs.md, docs/pain-injuries.md,\n  mindmap.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-647 - Objectifs - Prédictions de course : ajustement selon une blessure active",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/136_add_injury_prediction_adjustment.sql",
        "docs/objectifs.md",
        "docs/pain-injuries.md",
        "src/Domain/Training/InjuryPredictionAdjustment.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Goals/GoalFormHelpers.php",
        "src/Http/Goals/GoalsController.php",
        "src/Http/Goals/RaceDetailController.php",
        "src/Http/Goals/RaceDetailPresenter.php",
        "src/Http/Goals/RacesController.php",
        "src/Http/Sessions/RaceComparisonPresenter.php",
        "src/Http/Settings/SettingsController.php",
        "src/Http/Training/InjuryAdjustmentContext.php",
        "src/Http/Training/RaceProjection.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dashboard.php",
        "templates/goals.php",
        "templates/partials/goal_form_fields.php",
        "templates/partials/injury_projection_icon.php",
        "templates/races.php",
        "templates/races_detail.php",
        "templates/session_detail.php",
        "templates/settings.php",
        "tests/Domain/Training/InjuryPredictionAdjustmentTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 84,
          "deletions": 0,
          "lines_changed": 84
        },
        {
          "path": "config/routes.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "db/migrations/136_add_injury_prediction_adjustment.sql",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "docs/pain-injuries.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "src/Domain/Training/InjuryPredictionAdjustment.php",
          "insertions": 69,
          "deletions": 0,
          "lines_changed": 69
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 7,
          "deletions": 3,
          "lines_changed": 10
        },
        {
          "path": "src/Http/Goals/GoalFormHelpers.php",
          "insertions": 27,
          "deletions": 3,
          "lines_changed": 30
        },
        {
          "path": "src/Http/Goals/GoalsController.php",
          "insertions": 17,
          "deletions": 7,
          "lines_changed": 24
        },
        {
          "path": "src/Http/Goals/RaceDetailController.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Goals/RaceDetailPresenter.php",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 45,
          "deletions": 5,
          "lines_changed": 50
        },
        {
          "path": "src/Http/Sessions/RaceComparisonPresenter.php",
          "insertions": 25,
          "deletions": 1,
          "lines_changed": 26
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 9,
          "deletions": 2,
          "lines_changed": 11
        },
        {
          "path": "src/Http/Training/InjuryAdjustmentContext.php",
          "insertions": 114,
          "deletions": 0,
          "lines_changed": 114
        },
        {
          "path": "src/Http/Training/RaceProjection.php",
          "insertions": 24,
          "deletions": 3,
          "lines_changed": 27
        },
        {
          "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/dashboard.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/goals.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/partials/goal_form_fields.php",
          "insertions": 20,
          "deletions": 1,
          "lines_changed": 21
        },
        {
          "path": "templates/partials/injury_projection_icon.php",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        },
        {
          "path": "templates/races.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/races_detail.php",
          "insertions": 16,
          "deletions": 1,
          "lines_changed": 17
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 7,
          "deletions": 4,
          "lines_changed": 11
        },
        {
          "path": "templates/settings.php",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "tests/Domain/Training/InjuryPredictionAdjustmentTest.php",
          "insertions": 86,
          "deletions": 0,
          "lines_changed": 86
        }
      ],
      "files_changed": 28,
      "insertions": 684,
      "deletions": 35,
      "tags": [
        "v1.3.13"
      ]
    },
    {
      "hash": "026798c4fb29c903f3bced185cc36c5adce3926c",
      "short_hash": "026798c",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788126877,
      "message": "SPOR-646 - Météo - Widget Dashboard : halo coloré des tuiles de prévision\n\nProlonge les couleurs sémantiques des métriques (SPOR-634 addendum) à chaque\ntuile de prévision (3 horaires + 7 jours) : halo discret + icône colorée\nselon la condition de CETTE tuile.\n\n- Domain\\Weather\\WeatherCode::tileSeverity(int $code): ?string (pur, testé) :\n  'danger' (verglas 56/57/66/67, neige forte 75/86) / 'warn' (orage 95/96/99)\n  / 'rain' (pluie/averses/bruine + neige légère) / null.\n- WeatherPanel::hourlyView()/dailyView() : clé severity.\n- templates/dashboard.php : class rv-wx-tile rv-wx-tile--{severity} + data-color\n  de même ton sur l'icône de chaque tuile.\n- components.css : .rv-wx-tile (bordure neutre) + .rv-wx-tile--rain/--warn/\n  --danger = box-shadow halo doux (vert/orange/rouge). Rien dans References/design/.\n- i18n : aucun (marquage purement visuel).\n- Doc : TECHNICAL.md, docs/meteo.md, mindmap.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-646 - Météo - Widget Dashboard : halo coloré des tuiles de prévision",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/meteo.md",
        "public/assets/css/components.css",
        "src/Domain/Weather/WeatherCode.php",
        "src/Http/Weather/WeatherPanel.php",
        "templates/dashboard.php",
        "tests/Domain/Weather/WeatherCodeTest.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": "docs/meteo.md",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "src/Domain/Weather/WeatherCode.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "src/Http/Weather/WeatherPanel.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 23,
          "deletions": 4,
          "lines_changed": 27
        },
        {
          "path": "tests/Domain/Weather/WeatherCodeTest.php",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        }
      ],
      "files_changed": 8,
      "insertions": 158,
      "deletions": 4
    },
    {
      "hash": "6764a56716c0938d27632678c8632e5317d4867c",
      "short_hash": "6764a56",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788122452,
      "message": "fix: Add more commit for release notes",
      "message_short": "fix: Add more commit for release notes",
      "type": "fix",
      "modified_files": [
        ".github/workflows/deploy.yml"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        }
      ],
      "files_changed": 1,
      "insertions": 2,
      "deletions": 2
    },
    {
      "hash": "91e3ab8d3002561b11068c7ff12aa25a9518b5bd",
      "short_hash": "91e3ab8",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788122412,
      "message": "SPOR-645 - Dashboard - Carte « Prochaine course » : plusieurs courses le même jour\n\nLe LIMIT 1 de nextRace() masquait toute course partageant la due_date la\nplus proche (relais + course perso, enchaînement sur un week-end) jusqu'à\nce que ce jour soit passé.\n\n- Domain\\Training\\NextRaceDay::nearest() (pur, testé) : à partir des lignes\n  triées due_date ASC/id ASC, ne retient que celles de la date la plus\n  proche et ré-indexe.\n- DashboardController::nextRace() -> nextRaces() : SELECT sans LIMIT,\n  retourne {isToday, dueLabel, daysLabel, races[]} — date/compte à rebours/\n  Jour J partagés, projection (SPOR-275) et miniature (SPOR-617) par course.\n- templates/dashboard.php : une carte avec liste interne, une ligne\n  <a href=\"/races/{id}\"> par course, entête « Prochaines courses » si > 1.\n- i18n FR/EN : dashboard.next_races.\n- Doc : TECHNICAL.md, docs/dashboard.md, mindmap.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-645 - Dashboard - Carte « Prochaine course » : plusieurs courses le même jour",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/dashboard.md",
        "src/Domain/Training/NextRaceDay.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Goals/GoalFormHelpers.php",
        "src/Http/Training/RaceProjection.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dashboard.php",
        "tests/Domain/NextRaceDayTest.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": "docs/dashboard.md",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "src/Domain/Training/NextRaceDay.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 61,
          "deletions": 45,
          "lines_changed": 106
        },
        {
          "path": "src/Http/Goals/GoalFormHelpers.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Training/RaceProjection.php",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "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": "templates/dashboard.php",
          "insertions": 59,
          "deletions": 45,
          "lines_changed": 104
        },
        {
          "path": "tests/Domain/NextRaceDayTest.php",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        }
      ],
      "files_changed": 11,
      "insertions": 289,
      "deletions": 95
    },
    {
      "hash": "88f052eb9933b4395e48818db720d9ff8f5f8cee",
      "short_hash": "88f052e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788120660,
      "message": "SPOR-644 - Doc : note CI « HEAD détaché sur checkout de tag » (TECHNICAL.md)\n\nTrace la raison du `git branch -f main HEAD` ajouté dans deploy.yml\n(échec v1.3.11, corrigé v1.3.12).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-644 - Doc : note CI « HEAD détaché sur checkout de tag » (TECHNICAL.md)",
      "type": "docs",
      "modified_files": [
        "TECHNICAL.md"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        }
      ],
      "files_changed": 1,
      "insertions": 5,
      "deletions": 1
    },
    {
      "hash": "a8738107e993cd5b5096f26be06df77f65c5f78d",
      "short_hash": "a873810",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788120400,
      "message": "SPOR-644 - CI/CD - deploy : rendre 'main' resolvable pour release_notes.py\n\nLe workflow se déclenche sur un push de tag -> actions/checkout laisse le\ndépôt en HEAD détaché, sans ref locale `main`. release_notes.py cible la\nbranche `main` par défaut, d'où `git rev-list ... main` -> \"fatal: bad\nrevision 'main'\" (deploy v1.3.11 en échec sur cette étape).\n\nFix : `git branch -f main HEAD` avant l'étape de génération.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-644 - CI/CD - deploy : rendre 'main' resolvable pour release_notes.py",
      "type": "ops",
      "modified_files": [
        ".github/workflows/deploy.yml"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        }
      ],
      "files_changed": 1,
      "insertions": 7,
      "deletions": 0,
      "tags": [
        "v1.3.12"
      ]
    },
    {
      "hash": "c168a95127bde95f2b9226b34abd1b5b4fecbdd0",
      "short_hash": "c168a95",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788119716,
      "message": "SPOR-644 - Version : lien vers les release notes (WebReleaseNotes)\n\nLe numéro de version (footer du site + pied de sidebar de l'app) devient un\nlien vers /release_notes.html, page statique générée à la CI par l'outil\nzRenard/WebReleaseNotes.\n\n- deploy.yml : release-downloader (dernière release WebReleaseNotes) +\n  setup-python + GitPython + `release_notes.py --num_commits 50` ; les 4\n  fichiers release_notes.{html,json,js,css} copiés dans dist/public/.\n- templates/partials/version_link.php : partial partagé foot.php + app_sidebar.php.\n  Le lien n'apparaît que si public/release_notes.html existe réellement\n  (toujours en prod ; en local si on copie les fichiers pour tester) — sinon\n  simple texte. Pas de couplage au numéro de version.\n- app.css / site.css : style du lien (couleur héritée, souligné au survol).\n- .gitignore : /release_notes.* ET /public/release_notes.* (les copies de\n  test locales ne doivent jamais partir en prod via `cp -r public`).\n\nVérifié au curl : page absente = texte ; 4 fichiers dans public/ = lien\n(footer + sidebar), assets servis en text/css / text/javascript / application/json.\nTests verts (2071).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-644 - Version : lien vers les release notes (WebReleaseNotes)",
      "type": "other",
      "modified_files": [
        ".github/workflows/deploy.yml",
        ".gitignore",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/deploiement.md",
        "docs/site.md",
        "public/assets/css/app.css",
        "public/assets/css/site.css",
        "templates/partials/app_sidebar.php",
        "templates/partials/foot.php",
        "templates/partials/version_link.php"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 32,
          "deletions": 1,
          "lines_changed": 33
        },
        {
          "path": ".gitignore",
          "insertions": 7,
          "deletions": 1,
          "lines_changed": 8
        },
        {
          "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/deploiement.md",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "docs/site.md",
          "insertions": 9,
          "deletions": 3,
          "lines_changed": 12
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "public/assets/css/site.css",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "templates/partials/foot.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/partials/version_link.php",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        }
      ],
      "files_changed": 11,
      "insertions": 135,
      "deletions": 8,
      "tags": [
        "v1.3.11"
      ]
    },
    {
      "hash": "de2672bf82e203862ea57e0cd7a96fe4daf9c281",
      "short_hash": "de2672b",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788115394,
      "message": "SPOR-643 - Événements - Fiche : la modale « Ajouter des séances » passait sous la carte Leaflet\n\n.rv-modal-overlay est en z-index:20 ; leaflet.css place les panes d'une carte\nen z-index 400-700 et .leaflet-container n'établit pas de stacking context —\nles panes remontaient donc au-dessus de l'overlay de la modale.\n\nFix : .leaflet-container { isolation: isolate } dans app.css — stacking\ncontext local, niveau auto, sous l'overlay. Une ligne, globale, sans toucher\n.rv-modal-overlay ni la feuille vendorisée leaflet.css.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-643 - Événements - Fiche : la modale « Ajouter des séances » passait sous la carte Leaflet",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/css/app.css"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 22,
          "deletions": 0,
          "lines_changed": 22
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        }
      ],
      "files_changed": 3,
      "insertions": 33,
      "deletions": 0,
      "tags": [
        "v1.3.10"
      ]
    },
    {
      "hash": "23aa3bde7243170cf73b888e900673cead95313a",
      "short_hash": "23aa3bd",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788112771,
      "message": "SPOR-642 - App - Sidebar : version déployée en bas du menu latéral\n\nUne fois connecté, le footer public n'est plus visible : la version déployée\n($appVersion, injectée par ViewRenderer depuis SPOR-641) s'affiche désormais\ntout en bas de la sidebar, sous l'encart « Vie privée ». « dev » en local.\n\n- templates/partials/app_sidebar.php : rend $appVersion dans .rv-sidebar__version.\n- public/assets/css/app.css : classe .rv-sidebar__version (ajout projet, même\n  précédent que .rv-sidebar__group-label / SPOR-429).\n- Aucune logique PHP nouvelle. Vérifié au curl (/dashboard, /sessions) : dev\n  local, v2.1.0 avec un VERSION factice. Tests verts (2071).\n\nNon poussé.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-642 - App - Sidebar : version déployée en bas du menu latéral",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/site.md",
        "public/assets/css/app.css",
        "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": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "docs/site.md",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        }
      ],
      "files_changed": 5,
      "insertions": 32,
      "deletions": 0,
      "tags": [
        "v1.3.9"
      ]
    },
    {
      "hash": "5522c220b45c53f7f40f4979d6213eb95b415905",
      "short_hash": "5522c22",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788112487,
      "message": "SPOR-641 - Site - Footer : afficher la version déployée (dernier tag v*)\n\nLe footer du site vitrine (home, about, blog, roadmap, donate) affiche la\nversion déployée : « … · v1.4.2 », ou « · dev » en local.\n\n- src/Infra/AppVersion.php : lit <root>/VERSION (écrit par deploy.yml au tag),\n  trim, repli 'dev' si absent/vide. Source unique — public/healthcheck.php et\n  OpsController::healthcheck() délèguent désormais ici (fin de la triple copie).\n- ViewRenderer::capture() injecte $appVersion dans chaque vue (comme $sportsCatalog).\n- templates/partials/foot.php : rendu dans .rv-footer__meta.\n- .gitignore : /VERSION (généré à la CI, jamais commité).\n- AppVersionTest ; footer vérifié au curl (/ /about /roadmap /donate).\n\nTests verts (2071). Non poussé.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-641 - Site - Footer : afficher la version déployée (dernier tag v*)",
      "type": "other",
      "modified_files": [
        ".gitignore",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/deploiement.md",
        "docs/site.md",
        "public/healthcheck.php",
        "src/Http/Ops/OpsController.php",
        "src/Infra/AppVersion.php",
        "src/Infra/View/ViewRenderer.php",
        "templates/partials/foot.php",
        "tests/Infra/AppVersionTest.php"
      ],
      "modified_files_details": [
        {
          "path": ".gitignore",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "docs/deploiement.md",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "docs/site.md",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "public/healthcheck.php",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "src/Http/Ops/OpsController.php",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "src/Infra/AppVersion.php",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "src/Infra/View/ViewRenderer.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "templates/partials/foot.php",
          "insertions": 7,
          "deletions": 2,
          "lines_changed": 9
        },
        {
          "path": "tests/Infra/AppVersionTest.php",
          "insertions": 48,
          "deletions": 0,
          "lines_changed": 48
        }
      ],
      "files_changed": 11,
      "insertions": 152,
      "deletions": 7
    },
    {
      "hash": "dffeaa4555f54f261e636e1800ff1cdf4a995677",
      "short_hash": "dffeaa4",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788111972,
      "message": "SPOR-638/639/640 - Objectifs & Événements - Page de détail course + cohérence statut + partial séance\n\nSPOR-638 - Page de détail dédiée GET /races/{id} (lecture seule, tous statuts) :\ninfos course, cible + prévision, documents/checklist ; pour une course terminée,\nséance liée + résultat officiel + « prévision vs réalité » (Domain\\Training\\\nRacePlanOutcome, testé) + age grading. Route -> RaceDetailController, présentation\n-> RaceDetailPresenter ; balayage « clos à J+1 » extrait en OverdueRaceCloser.\nEntrées : carte /races cliquable (initCardLinks), fiche Événement, détail séance.\n\nSPOR-639 - EventRacePicker::forEvent() affichait « Planifié » au lieu de\n« Terminée » le jour même d'une course clôturée en avance par une séance liée :\nnouveau Domain\\Training\\RaceCompletion (status='completed' OU date passée) +\nalignement du ruban sur presentRace()/presentCompletedGoal() (séance liée =>\nFinished même sans « Registered »).\n\nSPOR-640 - La carte de séance de la fiche Événement était une copie réduite de\n.rv-session-card : extraction en templates/partials/session_card.php, partagé\navec /sessions, /dashboard et la timeline.\n\nTests verts (2068). Non poussé.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-638/639/640 - Objectifs & Événements - Page de détail course + cohérence statut + partial séanc",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/events.md",
        "docs/objectifs.md",
        "public/assets/js/interactions.js",
        "src/Domain/Training/RaceCompletion.php",
        "src/Domain/Training/RacePlanOutcome.php",
        "src/Http/Events/EventRacePicker.php",
        "src/Http/Events/EventsController.php",
        "src/Http/Goals/OverdueRaceCloser.php",
        "src/Http/Goals/RaceDetailController.php",
        "src/Http/Goals/RaceDetailPresenter.php",
        "src/Http/Goals/RacesController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dashboard.php",
        "templates/event_detail.php",
        "templates/partials/session_card.php",
        "templates/races.php",
        "templates/races_detail.php",
        "templates/session_detail.php",
        "templates/sessions.php",
        "templates/timeline.php",
        "tests/Domain/RaceCompletionTest.php",
        "tests/Domain/RacePlanOutcomeTest.php",
        "tests/Http/Events/SessionCardPartialTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 172,
          "deletions": 0,
          "lines_changed": 172
        },
        {
          "path": "config/routes.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "docs/events.md",
          "insertions": 7,
          "deletions": 4,
          "lines_changed": 11
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 40,
          "deletions": 1,
          "lines_changed": 41
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "src/Domain/Training/RaceCompletion.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "src/Domain/Training/RacePlanOutcome.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "src/Http/Events/EventRacePicker.php",
          "insertions": 14,
          "deletions": 2,
          "lines_changed": 16
        },
        {
          "path": "src/Http/Events/EventsController.php",
          "insertions": 12,
          "deletions": 1,
          "lines_changed": 13
        },
        {
          "path": "src/Http/Goals/OverdueRaceCloser.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "src/Http/Goals/RaceDetailController.php",
          "insertions": 87,
          "deletions": 0,
          "lines_changed": 87
        },
        {
          "path": "src/Http/Goals/RaceDetailPresenter.php",
          "insertions": 392,
          "deletions": 0,
          "lines_changed": 392
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 5,
          "deletions": 9,
          "lines_changed": 14
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 1,
          "deletions": 96,
          "lines_changed": 97
        },
        {
          "path": "templates/event_detail.php",
          "insertions": 10,
          "deletions": 42,
          "lines_changed": 52
        },
        {
          "path": "templates/partials/session_card.php",
          "insertions": 157,
          "deletions": 0,
          "lines_changed": 157
        },
        {
          "path": "templates/races.php",
          "insertions": 6,
          "deletions": 4,
          "lines_changed": 10
        },
        {
          "path": "templates/races_detail.php",
          "insertions": 314,
          "deletions": 0,
          "lines_changed": 314
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "templates/sessions.php",
          "insertions": 1,
          "deletions": 121,
          "lines_changed": 122
        },
        {
          "path": "templates/timeline.php",
          "insertions": 1,
          "deletions": 72,
          "lines_changed": 73
        },
        {
          "path": "tests/Domain/RaceCompletionTest.php",
          "insertions": 47,
          "deletions": 0,
          "lines_changed": 47
        },
        {
          "path": "tests/Domain/RacePlanOutcomeTest.php",
          "insertions": 52,
          "deletions": 0,
          "lines_changed": 52
        },
        {
          "path": "tests/Http/Events/SessionCardPartialTest.php",
          "insertions": 115,
          "deletions": 0,
          "lines_changed": 115
        }
      ],
      "files_changed": 27,
      "insertions": 1637,
      "deletions": 354
    },
    {
      "hash": "1b9447839a04d09c344136b08aa6059666640437",
      "short_hash": "1b94478",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788110107,
      "message": "SPOR-637 - Objectifs - Races : lier une séance à une course la marque « terminée »\n\nLier une séance (carte /races, date atteinte) clôture la course :\nstatus='completed', ruban « Finished » même sans « Registered ».\nUnlink réactive la course uniquement si sa date n'est pas passée.\n\n- migration 135 : back-fill des courses déjà liées\n- RaceReactivation / RaceRegistrationStatus : param hasLinkedSession\n- RacesController linkSession/unlinkSession/edit/presentRace\n- GoalFormHelpers::presentCompletedGoal\n- tests domaine + docs/objectifs.md + TECHNICAL.md\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-637 - Objectifs - Races : lier une séance à une course la marque « terminée »",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/135_complete_races_with_linked_session.sql",
        "docs/objectifs.md",
        "src/Domain/Training/RaceReactivation.php",
        "src/Domain/Training/RaceRegistrationStatus.php",
        "src/Http/Goals/GoalFormHelpers.php",
        "src/Http/Goals/RacesController.php",
        "tests/Domain/RaceReactivationTest.php",
        "tests/Domain/RaceRegistrationStatusTest.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": "db/migrations/135_complete_races_with_linked_session.sql",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "src/Domain/Training/RaceReactivation.php",
          "insertions": 14,
          "deletions": 3,
          "lines_changed": 17
        },
        {
          "path": "src/Domain/Training/RaceRegistrationStatus.php",
          "insertions": 15,
          "deletions": 3,
          "lines_changed": 18
        },
        {
          "path": "src/Http/Goals/GoalFormHelpers.php",
          "insertions": 10,
          "deletions": 1,
          "lines_changed": 11
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 49,
          "deletions": 10,
          "lines_changed": 59
        },
        {
          "path": "tests/Domain/RaceReactivationTest.php",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "tests/Domain/RaceRegistrationStatusTest.php",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        }
      ],
      "files_changed": 10,
      "insertions": 207,
      "deletions": 17
    },
    {
      "hash": "59d4591816fe6186743299a5d466710485c63669",
      "short_hash": "59d4591",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788109042,
      "message": "SPOR-634 - Météo - Bandes de couleur : couverture nuageuse + PM2.5 / PM10\n\n- WeatherMetricBand : cloudCover() (vert/jaune/orange, ni danger ni froid),\n  pm25() et pm10() (vert/jaune/orange/rouge, seuils alignés EAQI).\n- WeatherPanel : cloudBand sur `current`, pm25Band/pm10Band sur airQuality.\n- dashboard.php : couverture nuageuse colorisée dans les conditions du\n  moment ; PM10 ajouté à côté de PM2.5, les deux colorisés.\n\nTests : WeatherMetricBandTest étendu. Suite complète verte (2049 tests).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-634 - Météo - Bandes de couleur : couverture nuageuse + PM2.5 / PM10",
      "type": "other",
      "modified_files": [
        "TECHNICAL.md",
        "src/Domain/Weather/WeatherMetricBand.php",
        "src/Http/Weather/WeatherPanel.php",
        "templates/dashboard.php",
        "tests/Domain/Weather/WeatherMetricBandTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "src/Domain/Weather/WeatherMetricBand.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "src/Http/Weather/WeatherPanel.php",
          "insertions": 6,
          "deletions": 1,
          "lines_changed": 7
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 7,
          "deletions": 5,
          "lines_changed": 12
        },
        {
          "path": "tests/Domain/Weather/WeatherMetricBandTest.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        }
      ],
      "files_changed": 5,
      "insertions": 76,
      "deletions": 9
    },
    {
      "hash": "d3dd26ba84558bfd22f479859e0a79651b3a764b",
      "short_hash": "d3dd26b",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788108734,
      "message": "SPOR-634 - Météo - Couleurs sémantiques sur les métriques du widget Dashboard\n\nDemande explicite : coloriser les valeurs météo du Dashboard.\n\n- Domain/Weather/WeatherMetricBand (pur, testé) : classe une valeur en\n  ok (vert) / caution (jaune) / warn (orange) / danger (rouge) / cold\n  (gris, danger par froid extrême). Seuils = constantes documentées.\n  Métriques : température, rafales, probabilité de pluie, indice UV, EAQI,\n  pollens (rouge/orange/vert seulement, comme demandé).\n  Ex. température : ≥ 40 °C rouge, ≤ -20 °C gris, confortable = vert.\n- WeatherPanel : attache les bandes aux vues hourly/daily/current/airQuality.\n- templates/dashboard.php : map bande → couleur (--status-success /\n  --status-warn / --fox-500 / --status-danger / --text-muted), appliquée à\n  la température (courant + tuiles), au vent, à la pluie, aux UV, à l'EAQI et\n  aux pollens. La valeur chiffrée reste toujours visible.\n\nTest : WeatherMetricBandTest. Suite complète verte (2046 tests).\nTECHNICAL.md § Météo (addenda widget Dashboard).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-634 - Météo - Couleurs sémantiques sur les métriques du widget Dashboard",
      "type": "other",
      "modified_files": [
        "TECHNICAL.md",
        "src/Domain/Weather/WeatherMetricBand.php",
        "src/Http/Weather/WeatherPanel.php",
        "templates/dashboard.php",
        "tests/Domain/Weather/WeatherMetricBandTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 16,
          "deletions": 8,
          "lines_changed": 24
        },
        {
          "path": "src/Domain/Weather/WeatherMetricBand.php",
          "insertions": 101,
          "deletions": 0,
          "lines_changed": 101
        },
        {
          "path": "src/Http/Weather/WeatherPanel.php",
          "insertions": 27,
          "deletions": 6,
          "lines_changed": 33
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 29,
          "deletions": 12,
          "lines_changed": 41
        },
        {
          "path": "tests/Domain/Weather/WeatherMetricBandTest.php",
          "insertions": 73,
          "deletions": 0,
          "lines_changed": 73
        }
      ],
      "files_changed": 5,
      "insertions": 246,
      "deletions": 26
    },
    {
      "hash": "29d46cdc8439d1141566cf88146031266d75c2d0",
      "short_hash": "29d46cd",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788108073,
      "message": "SPOR-636 - Météo - Contexte météo par séance + affinages widget Dashboard\n\n## SPOR-636 — Météo par séance (Should)\n\nDeux instantanés météo par séance (conditions au départ et à l'arrivée, au\npoint de départ) : température + ressenti, code WMO, vent + rafales +\ndirection, précipitations, humidité. Carte « Météo » de l'onglet Summary du\ndétail de séance — rien de public (aucune vue publique de séance).\n\n- Migration 134 : sessions.weather_json (JSON {start,end}) +\n  weather_checked_at (sentinelle « tentée »).\n- Domain/Weather (pur) : SessionWeather (VO), SessionWeatherExtractor (heure\n  la plus proche, écart max 3 h), HistoricalWeatherEndpoint (archive ERA5\n  > 6 j / forecast past_days sinon).\n- OpenMeteoClient::history() + OpenMeteoResponseParser::parseHistory().\n- Infra/Weather/SessionWeatherService (DI `sessionWeather`) :\n  - prod : récupération PARESSEUSE au 1er affichage du détail\n    (SessionDetailController, 4e arg nullable) ; échec réseau → retenté ;\n  - local : bin/backfill_session_weather.php + public/ (ops_token +\n    throttle) → Infra/Db/SessionWeatherBackfill (lot 20, pause 200 ms).\n- Exclusions : pas de start_lat, ou sous-catégorie indoor\n  (IndoorActivity::LOCATIONLESS_SUBSPORTS) → weather_checked_at sans json.\n- Immuable → stocké sur la ligne `sessions`, pas de cache. i18n\n  sessions.weather.* (en+fr).\n\n## SPOR-634 addenda (demande explicite mid-turn)\n\n- Widget Dashboard : affiche la localisation réellement résolue — libellé\n  sinon coordonnées arrondies — suivie de la source.\n- « Prochaines 2 h » + « 7 jours » regroupés dans un <details> repliable\n  (natif, aucun JS) pour garder le Dashboard compact.\n\nTests : SessionWeatherTest, SessionWeatherExtractorTest,\nHistoricalWeatherEndpointTest, OpenMeteoResponseParserTest::parseHistory —\nsuite complète verte (2038 tests). TECHNICAL.md § Météo, docs/meteo.md,\ndocs/sessions.md, docs/dashboard.md, mindmap 🟢. Epic SPOR-631 → Done.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-636 - Météo - Contexte météo par séance + affinages widget Dashboard",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "bin/backfill_session_weather.php",
        "config/routes.php",
        "db/migrations/134_add_session_weather.sql",
        "docs/meteo.md",
        "docs/sessions.md",
        "public/backfill_session_weather.php",
        "public/index.php",
        "src/Domain/Weather/HistoricalWeatherEndpoint.php",
        "src/Domain/Weather/SessionWeather.php",
        "src/Domain/Weather/SessionWeatherExtractor.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Weather/SessionWeatherPresenter.php",
        "src/Http/Weather/WeatherPanel.php",
        "src/Infra/Db/SessionWeatherBackfill.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "src/Infra/Weather/OpenMeteoClient.php",
        "src/Infra/Weather/OpenMeteoResponseParser.php",
        "src/Infra/Weather/SessionWeatherService.php",
        "templates/dashboard.php",
        "templates/session_detail.php",
        "tests/Domain/Weather/HistoricalWeatherEndpointTest.php",
        "tests/Domain/Weather/SessionWeatherExtractorTest.php",
        "tests/Domain/Weather/SessionWeatherTest.php",
        "tests/Infra/Weather/OpenMeteoResponseParserTest.php",
        "tests/fixtures/weather/history.json"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 58,
          "deletions": 10,
          "lines_changed": 68
        },
        {
          "path": "bin/backfill_session_weather.php",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "config/routes.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "db/migrations/134_add_session_weather.sql",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "docs/meteo.md",
          "insertions": 22,
          "deletions": 4,
          "lines_changed": 26
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "public/backfill_session_weather.php",
          "insertions": 100,
          "deletions": 0,
          "lines_changed": 100
        },
        {
          "path": "public/index.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "src/Domain/Weather/HistoricalWeatherEndpoint.php",
          "insertions": 42,
          "deletions": 0,
          "lines_changed": 42
        },
        {
          "path": "src/Domain/Weather/SessionWeather.php",
          "insertions": 83,
          "deletions": 0,
          "lines_changed": 83
        },
        {
          "path": "src/Domain/Weather/SessionWeatherExtractor.php",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 22,
          "deletions": 1,
          "lines_changed": 23
        },
        {
          "path": "src/Http/Weather/SessionWeatherPresenter.php",
          "insertions": 67,
          "deletions": 0,
          "lines_changed": 67
        },
        {
          "path": "src/Http/Weather/WeatherPanel.php",
          "insertions": 12,
          "deletions": 2,
          "lines_changed": 14
        },
        {
          "path": "src/Infra/Db/SessionWeatherBackfill.php",
          "insertions": 58,
          "deletions": 0,
          "lines_changed": 58
        },
        {
          "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/Weather/OpenMeteoClient.php",
          "insertions": 60,
          "deletions": 5,
          "lines_changed": 65
        },
        {
          "path": "src/Infra/Weather/OpenMeteoResponseParser.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "src/Infra/Weather/SessionWeatherService.php",
          "insertions": 83,
          "deletions": 0,
          "lines_changed": 83
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 37,
          "deletions": 26,
          "lines_changed": 63
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "tests/Domain/Weather/HistoricalWeatherEndpointTest.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "tests/Domain/Weather/SessionWeatherExtractorTest.php",
          "insertions": 71,
          "deletions": 0,
          "lines_changed": 71
        },
        {
          "path": "tests/Domain/Weather/SessionWeatherTest.php",
          "insertions": 55,
          "deletions": 0,
          "lines_changed": 55
        },
        {
          "path": "tests/Infra/Weather/OpenMeteoResponseParserTest.php",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "tests/fixtures/weather/history.json",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        }
      ],
      "files_changed": 28,
      "insertions": 1124,
      "deletions": 50
    },
    {
      "hash": "57fd0583acb7a4deb21f9660d83faabf1ea562e3",
      "short_hash": "57fd058",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788105060,
      "message": "SPOR-631 - Météo - Prévision Dashboard 2h/7j + alertes contextuelles\n\nEpic Météo (SPOR-631), stories S1-S4 (SPOR-632/633/634/635) :\n\n- SPOR-632 Infra Open-Meteo : WeatherProvider + OpenMeteoClient (appel\n  serveur uniquement, pattern public/tiles.php, 0 fuite d'IP),\n  OpenMeteoResponseParser (pur), CachedWeatherProvider (cache BDD\n  weather_cache, TTL 30 min, sert le cache périmé sur panne réseau),\n  WeatherLog -> var/log/weather.log. Pas de cron : fetch au chargement\n  de /dashboard. Service `weather` dans public/index.php.\n- SPOR-633 Réglage « lieu d'entraînement habituel » : migration 131\n  (users.home_lat/lon/label, privé), onglet Settings « Lieu » avec\n  <form>/POST /settings/location dédié + mini-carte Leaflet (tuiles\n  /tiles.php), fallback sur la dernière séance outdoor\n  (WeatherLocationResolver).\n- SPOR-634 Widget Dashboard : Domain/Weather pur (WeatherForecast,\n  WeatherCode, ForecastWindow), WeatherPanel, carte dashboard.php\n  (2 h + 7 jours glissants + qualité de l'air/pollens), icônes météo,\n  i18n weather.* en+fr.\n- SPOR-635 Alerte contextuelle : UsualTrainingWindow + WeatherAlertRules\n  (gel/vent/pluie/UV sur le créneau habituel), bandeau rv-alert--warn,\n  migration 133 (source_type weather_alert) + WeatherNotification\n  idempotente (une alerte/type/jour).\n\nRien de public. Dégradation gracieuse : /health intact, incidents dans\n/admin/logs. Tests : 42 tests météo, suite complète verte.\nDoc : TECHNICAL.md § Météo, docs/meteo.md, docs/dashboard.md.\nMétéo par séance (SPOR-636) : backlog.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-631 - Météo - Prévision Dashboard 2h/7j + alertes contextuelles",
      "type": "other",
      "modified_files": [
        "CLAUDE.md",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/131_add_users_home_location.sql",
        "db/migrations/132_add_weather_cache.sql",
        "db/migrations/133_add_weather_alert_notification_type.sql",
        "docs/README.md",
        "docs/dashboard.md",
        "docs/meteo.md",
        "public/assets/js/home-location-picker.js",
        "public/assets/js/icons.js",
        "public/index.php",
        "src/Domain/Weather/ForecastWindow.php",
        "src/Domain/Weather/HomeLocationInput.php",
        "src/Domain/Weather/UsualTrainingWindow.php",
        "src/Domain/Weather/WeatherAlert.php",
        "src/Domain/Weather/WeatherAlertRules.php",
        "src/Domain/Weather/WeatherCode.php",
        "src/Domain/Weather/WeatherForecast.php",
        "src/Domain/Weather/WeatherLocationKey.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Settings/SettingsController.php",
        "src/Http/Weather/WeatherLocationResolver.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/Weather/CachedWeatherProvider.php",
        "src/Infra/Weather/OpenMeteoClient.php",
        "src/Infra/Weather/OpenMeteoResponseParser.php",
        "src/Infra/Weather/WeatherLog.php",
        "src/Infra/Weather/WeatherProvider.php",
        "src/Infra/Weather/WeatherUnavailable.php",
        "templates/dashboard.php",
        "templates/settings.php",
        "tests/Domain/Weather/ForecastWindowTest.php",
        "tests/Domain/Weather/HomeLocationInputTest.php",
        "tests/Domain/Weather/UsualTrainingWindowTest.php",
        "tests/Domain/Weather/WeatherAlertRulesTest.php",
        "tests/Domain/Weather/WeatherCodeTest.php",
        "tests/Domain/Weather/WeatherForecastTest.php",
        "tests/Domain/Weather/WeatherLocationKeyTest.php",
        "tests/Infra/Weather/OpenMeteoResponseParserTest.php",
        "tests/fixtures/weather/air_quality.json",
        "tests/fixtures/weather/forecast.json"
      ],
      "modified_files_details": [
        {
          "path": "CLAUDE.md",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 121,
          "deletions": 0,
          "lines_changed": 121
        },
        {
          "path": "config/routes.php",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        },
        {
          "path": "db/migrations/131_add_users_home_location.sql",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "db/migrations/132_add_weather_cache.sql",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "db/migrations/133_add_weather_alert_notification_type.sql",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "docs/README.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "docs/dashboard.md",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "docs/meteo.md",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "public/assets/js/home-location-picker.js",
          "insertions": 114,
          "deletions": 0,
          "lines_changed": 114
        },
        {
          "path": "public/assets/js/icons.js",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "public/index.php",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "src/Domain/Weather/ForecastWindow.php",
          "insertions": 58,
          "deletions": 0,
          "lines_changed": 58
        },
        {
          "path": "src/Domain/Weather/HomeLocationInput.php",
          "insertions": 77,
          "deletions": 0,
          "lines_changed": 77
        },
        {
          "path": "src/Domain/Weather/UsualTrainingWindow.php",
          "insertions": 58,
          "deletions": 0,
          "lines_changed": 58
        },
        {
          "path": "src/Domain/Weather/WeatherAlert.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "src/Domain/Weather/WeatherAlertRules.php",
          "insertions": 120,
          "deletions": 0,
          "lines_changed": 120
        },
        {
          "path": "src/Domain/Weather/WeatherCode.php",
          "insertions": 92,
          "deletions": 0,
          "lines_changed": 92
        },
        {
          "path": "src/Domain/Weather/WeatherForecast.php",
          "insertions": 86,
          "deletions": 0,
          "lines_changed": 86
        },
        {
          "path": "src/Domain/Weather/WeatherLocationKey.php",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 12,
          "deletions": 2,
          "lines_changed": 14
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 76,
          "deletions": 1,
          "lines_changed": 77
        },
        {
          "path": "src/Http/Weather/WeatherLocationResolver.php",
          "insertions": 82,
          "deletions": 0,
          "lines_changed": 82
        },
        {
          "path": "src/Http/Weather/WeatherNotification.php",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "src/Http/Weather/WeatherPanel.php",
          "insertions": 194,
          "deletions": 0,
          "lines_changed": 194
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 81,
          "deletions": 0,
          "lines_changed": 81
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        },
        {
          "path": "src/Infra/Weather/CachedWeatherProvider.php",
          "insertions": 114,
          "deletions": 0,
          "lines_changed": 114
        },
        {
          "path": "src/Infra/Weather/OpenMeteoClient.php",
          "insertions": 113,
          "deletions": 0,
          "lines_changed": 113
        },
        {
          "path": "src/Infra/Weather/OpenMeteoResponseParser.php",
          "insertions": 162,
          "deletions": 0,
          "lines_changed": 162
        },
        {
          "path": "src/Infra/Weather/WeatherLog.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "src/Infra/Weather/WeatherProvider.php",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "src/Infra/Weather/WeatherUnavailable.php",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 113,
          "deletions": 2,
          "lines_changed": 115
        },
        {
          "path": "templates/settings.php",
          "insertions": 75,
          "deletions": 1,
          "lines_changed": 76
        },
        {
          "path": "tests/Domain/Weather/ForecastWindowTest.php",
          "insertions": 81,
          "deletions": 0,
          "lines_changed": 81
        },
        {
          "path": "tests/Domain/Weather/HomeLocationInputTest.php",
          "insertions": 79,
          "deletions": 0,
          "lines_changed": 79
        },
        {
          "path": "tests/Domain/Weather/UsualTrainingWindowTest.php",
          "insertions": 56,
          "deletions": 0,
          "lines_changed": 56
        },
        {
          "path": "tests/Domain/Weather/WeatherAlertRulesTest.php",
          "insertions": 139,
          "deletions": 0,
          "lines_changed": 139
        },
        {
          "path": "tests/Domain/Weather/WeatherCodeTest.php",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "tests/Domain/Weather/WeatherForecastTest.php",
          "insertions": 59,
          "deletions": 0,
          "lines_changed": 59
        },
        {
          "path": "tests/Domain/Weather/WeatherLocationKeyTest.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "tests/Infra/Weather/OpenMeteoResponseParserTest.php",
          "insertions": 76,
          "deletions": 0,
          "lines_changed": 76
        },
        {
          "path": "tests/fixtures/weather/air_quality.json",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "tests/fixtures/weather/forecast.json",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        }
      ],
      "files_changed": 46,
      "insertions": 2855,
      "deletions": 7
    },
    {
      "hash": "cad356349f74dfd1e076a2a7c319ffb684a48285",
      "short_hash": "cad3563",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788095284,
      "message": "SPOR-626/627/628/629/630 - Badges - Catégories en entité + catégorie « enchaînement » séquentielle\n\nTransforme les catégories de badge (jusqu'ici texte libre, SPOR-510) en\nentité, avec une case « enchaînement » qui fait s'acquérir les badges d'une\ncatégorie l'un après l'autre.\n\n- SPOR-626 : table badge_categories (scope, name, is_sequential,\n  display_order) + migration 130 + BadgeCategoryStore + AdminBadgeCategories\n  Controller (CRUD) + section « Categories » sur /admin/badges + champ\n  catégorie du formulaire badge = <select> par scope.\n- SPOR-627 : SequentialBadgeChain (pur) — cibles incrémentales → seuils\n  absolus cumulés (prefix sum), report du surplus, progression du segment.\n  BadgeCatalog::load()/evaluate() : chaîne isolée en groupe cumulatif propre,\n  CumulativeBadges inchangé. Garde-fous : type unique + pas de fenêtre.\n- SPOR-628 : glisser-déposer des badges et des catégories dans /admin/badges\n  (réutilise initDragReorder) ; seed court→long, ensuite modifiable.\n- SPOR-629 : /badges affiche la cible/progression DU SEGMENT pour un badge\n  de chaîne, état « Locked » pour un palier verrouillé, étiquette « Chain ».\n- SPOR-630 : filtre par catégorie sur le tableau des badges, entre la\n  section Catégories et la table.\n\nTests : SequentialBadgeChainTest (7), BadgeCatalogTest (+4),\nBadgeCategoryStoreTest (4). Suite verte (1983 tests, 5356 assertions).\nDocs : TECHNICAL.md, docs/badges.md, docs/admin.md, mindmap.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-626/627/628/629/630 - Badges - Catégories en entité + catégorie « enchaînement » séquentielle",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/130_add_badge_categories.sql",
        "docs/admin.md",
        "docs/badges.md",
        "src/Domain/Training/SequentialBadgeChain.php",
        "src/Http/Admin/AdminBadgeCategoriesController.php",
        "src/Http/Admin/AdminBadgesController.php",
        "src/Http/Records/BadgeCatalog.php",
        "src/Http/Records/BadgeCategoryStore.php",
        "src/Http/Records/BadgesController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/admin_badges.php",
        "templates/badges.php",
        "templates/partials/badge_form_fields.php",
        "tests/Domain/SequentialBadgeChainTest.php",
        "tests/Http/Records/BadgeCatalogTest.php",
        "tests/Http/Records/BadgeCategoryStoreTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 117,
          "deletions": 0,
          "lines_changed": 117
        },
        {
          "path": "config/routes.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "db/migrations/130_add_badge_categories.sql",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "docs/admin.md",
          "insertions": 31,
          "deletions": 5,
          "lines_changed": 36
        },
        {
          "path": "docs/badges.md",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "src/Domain/Training/SequentialBadgeChain.php",
          "insertions": 71,
          "deletions": 0,
          "lines_changed": 71
        },
        {
          "path": "src/Http/Admin/AdminBadgeCategoriesController.php",
          "insertions": 233,
          "deletions": 0,
          "lines_changed": 233
        },
        {
          "path": "src/Http/Admin/AdminBadgesController.php",
          "insertions": 118,
          "deletions": 14,
          "lines_changed": 132
        },
        {
          "path": "src/Http/Records/BadgeCatalog.php",
          "insertions": 92,
          "deletions": 0,
          "lines_changed": 92
        },
        {
          "path": "src/Http/Records/BadgeCategoryStore.php",
          "insertions": 263,
          "deletions": 0,
          "lines_changed": 263
        },
        {
          "path": "src/Http/Records/BadgesController.php",
          "insertions": 27,
          "deletions": 6,
          "lines_changed": 33
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 18,
          "deletions": 1,
          "lines_changed": 19
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 18,
          "deletions": 1,
          "lines_changed": 19
        },
        {
          "path": "templates/admin_badges.php",
          "insertions": 214,
          "deletions": 64,
          "lines_changed": 278
        },
        {
          "path": "templates/badges.php",
          "insertions": 20,
          "deletions": 5,
          "lines_changed": 25
        },
        {
          "path": "templates/partials/badge_form_fields.php",
          "insertions": 25,
          "deletions": 19,
          "lines_changed": 44
        },
        {
          "path": "tests/Domain/SequentialBadgeChainTest.php",
          "insertions": 81,
          "deletions": 0,
          "lines_changed": 81
        },
        {
          "path": "tests/Http/Records/BadgeCatalogTest.php",
          "insertions": 97,
          "deletions": 0,
          "lines_changed": 97
        },
        {
          "path": "tests/Http/Records/BadgeCategoryStoreTest.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        }
      ],
      "files_changed": 20,
      "insertions": 1537,
      "deletions": 115
    },
    {
      "hash": "8a3c3129b81618c7a0da9420d2d55dba5a74188a",
      "short_hash": "8a3c312",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788091808,
      "message": "SPOR-625 - Sessions - Pastille de comptage sur l'onglet Tags (+ Équipement / Blessure en direct)\n\nAjoute une pastille .rv-tab__badge sur l'onglet Tags de la modale d'édition\nde séance et rend les trois onglets à pastille (Tags, Équipement, Blessure)\nlive : le compteur se recalcule à chaque ajout/retrait, sans rechargement.\n\n- SessionEditTabBadges (domaine pur, testé) centralise le calcul des trois\n  compteurs, jusque-là dupliqué inline dans la vue.\n- session_detail.php : les 3 boutons portent data-tab-badge ; rendu initial\n  des pastilles via le domaine.\n- interactions.js : initSessionTabBadges() recalcule en direct (change +\n  MutationObserver), sans aller-retour serveur ni champ soumis nouveau.\n- Tests : SessionEditTabBadgesTest (7 cas). Suite verte (1968 tests).\n- Docs : TECHNICAL.md, docs/sessions.md, mindmap.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-625 - Sessions - Pastille de comptage sur l'onglet Tags (+ Équipement / Blessure en direct)",
      "type": "other",
      "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"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 43,
          "deletions": 0,
          "lines_changed": 43
        },
        {
          "path": "docs/sessions.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 68,
          "deletions": 0,
          "lines_changed": 68
        },
        {
          "path": "src/Domain/Training/SessionEditTabBadges.php",
          "insertions": 62,
          "deletions": 0,
          "lines_changed": 62
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 12,
          "deletions": 3,
          "lines_changed": 15
        },
        {
          "path": "tests/Domain/SessionEditTabBadgesTest.php",
          "insertions": 54,
          "deletions": 0,
          "lines_changed": 54
        }
      ],
      "files_changed": 7,
      "insertions": 243,
      "deletions": 3
    },
    {
      "hash": "844d8ff67f9dc79ea7d4c83e6fcf23d8d88edf6f",
      "short_hash": "844d8ff",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788042855,
      "message": "SPOR-624 - Dashboard - Carte \"Prochaine course\" mise en avant le jour J\n\nLe jour de la course, la carte \"Prochaine course\" du Dashboard prend un\nlisere + halo orange (rv-card--accent / rv-card--glow-fox), l'entete passe\na \"Jour J\" et une ligne \"Bonne course !\" (icone trophy) s'affiche sous le\nnom.\n\n- DashboardController::nextRace() : cle isToday = GoalCountdown::daysUntil()===0\n  (meme convention serveur que le ruban \"En cours\", SPOR-623)\n- templates/dashboard.php : branche isToday (classes, entete, encouragement)\n- tokens.css : --glow-fox ; components.css : .rv-card--glow-fox\n- i18n dashboard.race_day / dashboard.race_day_cheer (FR/EN)\n- GoalCountdownTest : cas \"meme jour, heures differentes -> 0\"\n- TECHNICAL.md, docs/dashboard.md, mindmap\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-624 - Dashboard - Carte \"Prochaine course\" mise en avant le jour J",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/dashboard.md",
        "public/assets/css/components.css",
        "public/assets/css/tokens.css",
        "src/Http/Dashboard/DashboardController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dashboard.php",
        "tests/Domain/GoalCountdownTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        },
        {
          "path": "docs/dashboard.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "public/assets/css/tokens.css",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 6,
          "deletions": 1,
          "lines_changed": 7
        },
        {
          "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": 10,
          "deletions": 3,
          "lines_changed": 13
        },
        {
          "path": "tests/Domain/GoalCountdownTest.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        }
      ],
      "files_changed": 10,
      "insertions": 75,
      "deletions": 4,
      "tags": [
        "v1.3.8"
      ]
    },
    {
      "hash": "b4a8e2e51c2146289d676b382321a49c08791e10",
      "short_hash": "b4a8e2e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788042202,
      "message": "SPOR-623 - Objectifs - Races : ruban \"En cours\" le jour J\n\nLe jour de la course, une course active ET inscrite affiche desormais le\nruban bleu \"En cours\" (slug ongoing) au lieu de \"Planifiee\". Une course non\ninscrite reste \"En reflexion\" ce jour-la.\n\n- RaceRegistrationStatus::resolve() : 3e param bool $isToday = false\n- RacesController::presentRace() : isToday via GoalCountdown::daysUntil()===0\n  (meme convention serveur que completeOverdueRaces()) + libelle ongoing\n- i18n races.ribbon_ongoing (FR \"En cours\" / EN \"In progress\")\n- CSS : .rv-corner-ribbon--considering dissocie de --ongoing (partageaient\n  le bleu --status-info) -> considering passe a --aurora-violet\n- RaceRegistrationStatusTest : +3 cas ; TECHNICAL.md, docs/objectifs.md, mindmap\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-623 - Objectifs - Races : ruban \"En cours\" le jour J",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/objectifs.md",
        "public/assets/css/app.css",
        "src/Domain/Training/RaceRegistrationStatus.php",
        "src/Http/Goals/RacesController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "tests/Domain/RaceRegistrationStatusTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 22,
          "deletions": 0,
          "lines_changed": 22
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 12,
          "deletions": 9,
          "lines_changed": 21
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 6,
          "deletions": 1,
          "lines_changed": 7
        },
        {
          "path": "src/Domain/Training/RaceRegistrationStatus.php",
          "insertions": 16,
          "deletions": 6,
          "lines_changed": 22
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 12,
          "deletions": 3,
          "lines_changed": 15
        },
        {
          "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": "tests/Domain/RaceRegistrationStatusTest.php",
          "insertions": 29,
          "deletions": 0,
          "lines_changed": 29
        }
      ],
      "files_changed": 9,
      "insertions": 100,
      "deletions": 19
    },
    {
      "hash": "80d4d7f6fa9cd79b005af9b6e093accb5aaa0ed0",
      "short_hash": "80d4d7f",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788041617,
      "message": "SPOR-622 - Sessions - Popin temps + valeur au clic sur les graphes du resume\n\nClic sur une courbe de l'onglet Summary : repere synchronise (trait + point)\na la meme fraction horizontale sur les 5 graphes, chacun avec un popin\n\"temps . valeur\". Fermeture au clic ailleurs ou touche Echap.\n\n- SessionPresenter::lineChartPayload() : + unit / valueFormat / durationSeconds\n  (metadonnees popin, sans effet sur le trace)\n- session-charts.js : instances conservees, options.onClick -> openScrubber(),\n  repere + popin ancres dans .rv-trendchart__plot (coords locales -> suivent\n  le graphe au defilement de .rv-screen)\n- components.css : .rv-chart-scrubber / __dot / -popin\n- Tests SessionPresenterTest adaptes + testLineChartPayloadCarriesScrubberMetadata\n- TECHNICAL.md, docs/sessions.md, mindmap\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-622 - Sessions - Popin temps + valeur au clic sur les graphes du resume",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sessions.md",
        "public/assets/css/components.css",
        "public/assets/js/session-charts.js",
        "src/Http/Sessions/SessionDetailController.php",
        "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": 78,
          "deletions": 0,
          "lines_changed": 78
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "public/assets/js/session-charts.js",
          "insertions": 172,
          "deletions": 6,
          "lines_changed": 178
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 17,
          "deletions": 5,
          "lines_changed": 22
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 25,
          "deletions": 4,
          "lines_changed": 29
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 36,
          "deletions": 2,
          "lines_changed": 38
        }
      ],
      "files_changed": 8,
      "insertions": 363,
      "deletions": 17
    },
    {
      "hash": "c48c6477e68e510568ed34670cfda8c178af81a5",
      "short_hash": "c48c647",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788039053,
      "message": "SPOR-621 - Auth - Duree de vie des sessions : inactivite 36h + plafond absolu 10j\n\nDemande explicite de l'utilisateur : allonger la deconnexion pour\ninactivite. Avant, aucune expiration applicative (cookie de session pur +\ngc_maxlifetime ~24 min herite de l'hebergeur).\n\n- Cookie de session conserve (lifetime 0, mort a la fermeture du navigateur).\n- Inactivite 36h glissante : last_activity rafraichi a chaque requete.\n- Plafond absolu 10j depuis le login (login_at), prioritaire.\n- Memes valeurs dev et prod.\n\n- src/Domain/Auth/SessionLifetime.php : regle pure\n  expiryReason(lastActivityAt, loginAt, now) -> 'absolute'|'idle'|null,\n  bornes surchargeables (config). Teste (8 cas).\n- src/Http/Auth/SessionBootstrap.php : point d'entree unique partage par\n  public/index.php et public/tiles.php (cookie params + gc_maxlifetime +\n  save_path prive var/sessions/ en prod uniquement + controle d'expiration\n  + rafraichissement). tiles.php gagne un require vendor/autoload.php.\n- AuthGuard::login() pose login_at + last_activity.\n- config/settings.php : session_idle_ttl / session_absolute_ttl.\n- .gitignore (var/sessions/), docs/auth.md, TECHNICAL.md.\n\nDeploiement : au 1er deploiement prod, le save_path prive invalide les\nsessions existantes -> une reconnexion unique pour tous.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-621 - Auth - Duree de vie des sessions : inactivite 36h + plafond absolu 10j",
      "type": "other",
      "modified_files": [
        ".gitignore",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/settings.php",
        "docs/auth.md",
        "public/index.php",
        "public/tiles.php",
        "src/Domain/Auth/SessionLifetime.php",
        "src/Http/Auth/AuthGuard.php",
        "src/Http/Auth/SessionBootstrap.php",
        "tests/Domain/SessionLifetimeTest.php"
      ],
      "modified_files_details": [
        {
          "path": ".gitignore",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 79,
          "deletions": 0,
          "lines_changed": 79
        },
        {
          "path": "config/settings.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "docs/auth.md",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "public/index.php",
          "insertions": 4,
          "deletions": 6,
          "lines_changed": 10
        },
        {
          "path": "public/tiles.php",
          "insertions": 9,
          "deletions": 7,
          "lines_changed": 16
        },
        {
          "path": "src/Domain/Auth/SessionLifetime.php",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        },
        {
          "path": "src/Http/Auth/AuthGuard.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Auth/SessionBootstrap.php",
          "insertions": 83,
          "deletions": 0,
          "lines_changed": 83
        },
        {
          "path": "tests/Domain/SessionLifetimeTest.php",
          "insertions": 98,
          "deletions": 0,
          "lines_changed": 98
        }
      ],
      "files_changed": 11,
      "insertions": 366,
      "deletions": 13,
      "tags": [
        "v1.3.7"
      ]
    },
    {
      "hash": "4424ba918e17a98f8bbcad175b12e9c0de6dfbcb",
      "short_hash": "4424ba9",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788039039,
      "message": "SPOR-619 / SPOR-620 - Settings - Onglet Name : changer son nom + validation live et blocklist admin\n\nSPOR-619 — nouvel onglet Name dans /settings pour changer son nom /\nidentifiant (users.login). <form> separe (POST /settings/login), meme\ngarde-fou que submitPassword() : step-up 2FA, mot de passe actuel\nre-verifie, throttle \"change-login:<id>\", sleep(1) + SecurityLogger a\ncout constant, email de notification (locale du destinataire, envoi non\nbloquant). Aucune migration (colonne login depuis 001), pas de\nreconnexion. Domain\\Auth\\LoginChangeInput (pur, teste) pour la forme,\nunicite verifiee en base + try/catch sur la contrainte unique.\n\nSPOR-620 — blocklist de termes interdits dans un nom, administree sur\n/admin/blocked-names (ajout/suppression, pas d'edition). Filtre en\nsous-chaine insensible a la casse, applique au changement de nom ET a\nl'inscription (RegisterController). Verification live pendant la frappe :\nGET /settings/name/check -> JSON {status: ok|format|same|blocked|taken},\ndebounce 350 ms cote client (interactions.js initLoginNameValidation),\nsubmitLogin() reste la source de verite. Chaque ajout/suppression et\nchaque refus sont journalises dans security.log.\n\n- db/migrations/129_add_login_blocklist.sql\n- src/Domain/Auth/LoginBlocklist.php (pur, teste), LoginChangeInput.php (teste)\n- src/Http/Auth/LoginBlocklistStore.php, src/Http/Admin/AdminBlockedLoginsController.php\n- templates/admin_blocked_names.php, emails/login_changed{,_text}.php\n- i18n FR/EN, docs/admin.md + docs/profil.md, TECHNICAL.md\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-619 / SPOR-620 - Settings - Onglet Name : changer son nom + validation live et blocklist admin",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/129_add_login_blocklist.sql",
        "docs/admin.md",
        "docs/profil.md",
        "public/assets/js/interactions.js",
        "src/Domain/Auth/LoginBlocklist.php",
        "src/Domain/Auth/LoginChangeInput.php",
        "src/Http/Admin/AdminBlockedLoginsController.php",
        "src/Http/Auth/LoginBlocklistStore.php",
        "src/Http/Auth/RegisterController.php",
        "src/Http/Settings/SettingsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/admin_blocked_names.php",
        "templates/emails/login_changed.php",
        "templates/emails/login_changed_text.php",
        "templates/partials/admin_tabs.php",
        "templates/settings.php",
        "tests/Domain/LoginBlocklistTest.php",
        "tests/Domain/LoginChangeInputTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 200,
          "deletions": 0,
          "lines_changed": 200
        },
        {
          "path": "config/routes.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "db/migrations/129_add_login_blocklist.sql",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "docs/admin.md",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "docs/profil.md",
          "insertions": 40,
          "deletions": 12,
          "lines_changed": 52
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 75,
          "deletions": 0,
          "lines_changed": 75
        },
        {
          "path": "src/Domain/Auth/LoginBlocklist.php",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        },
        {
          "path": "src/Domain/Auth/LoginChangeInput.php",
          "insertions": 48,
          "deletions": 0,
          "lines_changed": 48
        },
        {
          "path": "src/Http/Admin/AdminBlockedLoginsController.php",
          "insertions": 135,
          "deletions": 0,
          "lines_changed": 135
        },
        {
          "path": "src/Http/Auth/LoginBlocklistStore.php",
          "insertions": 76,
          "deletions": 0,
          "lines_changed": 76
        },
        {
          "path": "src/Http/Auth/RegisterController.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 200,
          "deletions": 0,
          "lines_changed": 200
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "templates/admin_blocked_names.php",
          "insertions": 111,
          "deletions": 0,
          "lines_changed": 111
        },
        {
          "path": "templates/emails/login_changed.php",
          "insertions": 94,
          "deletions": 0,
          "lines_changed": 94
        },
        {
          "path": "templates/emails/login_changed_text.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "templates/partials/admin_tabs.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "templates/settings.php",
          "insertions": 62,
          "deletions": 1,
          "lines_changed": 63
        },
        {
          "path": "tests/Domain/LoginBlocklistTest.php",
          "insertions": 47,
          "deletions": 0,
          "lines_changed": 47
        },
        {
          "path": "tests/Domain/LoginChangeInputTest.php",
          "insertions": 80,
          "deletions": 0,
          "lines_changed": 80
        }
      ],
      "files_changed": 22,
      "insertions": 1394,
      "deletions": 14
    },
    {
      "hash": "c85092e31c2e6e582a1155e0beeb412cf94a4e3a",
      "short_hash": "c85092e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788038991,
      "message": "SPOR-618 - Tags de seance - Suggestions par seances similaires du meme sport\n\nNouvelle rangee \"Frequents pour ce sport\" dans l'onglet Tags de la modale\nd'edition, au-dessus de \"Previously used\" : tags tires de l'historique du\nMEME sport, ponderes par la proximite de distance/duree avec la seance\neditee. Puces .rv-chip--suggested (trait accent violet).\n\n- src/Domain/Training/SportTagSuggestions::rank() : pur, teste (9 cas) —\n  poids de proximite gaussien sur l'ecart relatif distance/duree\n  (SIGMA 0.5), seuil >= 3 seances, plafond 6, tri pertinence puis alpha.\n- src/Http/Sessions/SessionTagSuggestionsMap : une requete\n  sessions JOIN session_tags, rank() par sport, exclusions = tags deja\n  poses + catalogue actif (memes exclusions que SPOR-396).\n- SessionDetailController::render() : carte JSON par sport +\n  liste du sport courant, aucun changement dans edit().\n- templates/session_detail.php + interactions.js\n  (initSessionTagSportSuggestions, calquee sur initGearPickerOrder) :\n  reconstruit les puces au changement de sport, aucun aller-retour serveur.\n- CSS (.rv-chip--suggested), i18n FR/EN, doc (docs/sessions.md), TECHNICAL.md.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-618 - Tags de seance - Suggestions par seances similaires du meme sport",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sessions.md",
        "public/assets/css/components.css",
        "public/assets/js/interactions.js",
        "src/Domain/Training/SportTagSuggestions.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionTagSuggestionsMap.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/session_detail.php",
        "tests/Domain/SportTagSuggestionsTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 81,
          "deletions": 0,
          "lines_changed": 81
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 72,
          "deletions": 8,
          "lines_changed": 80
        },
        {
          "path": "src/Domain/Training/SportTagSuggestions.php",
          "insertions": 155,
          "deletions": 0,
          "lines_changed": 155
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "src/Http/Sessions/SessionTagSuggestionsMap.php",
          "insertions": 91,
          "deletions": 0,
          "lines_changed": 91
        },
        {
          "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": "templates/session_detail.php",
          "insertions": 23,
          "deletions": 2,
          "lines_changed": 25
        },
        {
          "path": "tests/Domain/SportTagSuggestionsTest.php",
          "insertions": 140,
          "deletions": 0,
          "lines_changed": 140
        }
      ],
      "files_changed": 12,
      "insertions": 587,
      "deletions": 10
    },
    {
      "hash": "9f094ce2fbd0d904cba896e1bf098da677664eab",
      "short_hash": "9f094ce",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788038948,
      "message": "SPOR-617 - Dashboard - Carte \"Next race\" : miniature de la photo de couverture de la course\n\nLa carte \"Next race\" du dashboard affiche desormais, a gauche, la\nminiature de la photo de couverture de la course (meme image que la carte\n/races, SPOR-511) quand elle existe ; sans photo, la carte s'affiche sans\nminiature.\n\n- src/Http/Dashboard/NextRacePhoto.php : construction de l'URL de\n  vignette (GET /races/{id}/image), null si pas de photo. Teste.\n- DashboardController::nextRace() : SELECT id + (image_content IS NOT NULL)\n  AS has_image (jamais le BLOB), expose raceId/hasImage/thumbnailUrl.\n- templates/dashboard.php : <img> 52x52 object-fit cover a gauche de la\n  carte, rien si thumbnailUrl est null.\n- doc utilisateur (docs/dashboard.md), TECHNICAL.md.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-617 - Dashboard - Carte \"Next race\" : miniature de la photo de couverture de la course",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/dashboard.md",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Dashboard/NextRacePhoto.php",
        "templates/dashboard.php",
        "tests/Http/Dashboard/NextRacePhotoTest.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": "docs/dashboard.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 22,
          "deletions": 7,
          "lines_changed": 29
        },
        {
          "path": "src/Http/Dashboard/NextRacePhoto.php",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 10,
          "deletions": 1,
          "lines_changed": 11
        },
        {
          "path": "tests/Http/Dashboard/NextRacePhotoTest.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        }
      ],
      "files_changed": 7,
      "insertions": 138,
      "deletions": 8
    },
    {
      "hash": "7e4ff1397d65c0ca66931f04f69036e9979c5a38",
      "short_hash": "7e4ff13",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788038915,
      "message": "SPOR-616 - Sessions - Modale d'edition : navigation seance precedente/suivante + garde-fou modifications non sauvegardees\n\nChevrons prev/next dans l'en-tete de la modale d'edition (memes voisins\nchronologiques que SPOR-430). Formulaire vierge -> navigation directe\nvers /sessions/{voisin}?edit=<onglet> ; formulaire modifie -> modale de\nconfirmation (Enregistrer & continuer / Abandonner & continuer / Annuler).\nEcran d'enregistrement (spinner) pendant toute soumission du formulaire.\n\n- src/Http/Sessions/EditModalTabs.php : mapping pur onglet <-> slug + sens\n  d'adjacence (then=prev|next), teste.\n- SessionDetailController : ?edit rouvre la modale sur l'onglet quitte ;\n  edit() recalcule l'adjacence sur started_at APRES update pour\n  \"Enregistrer & continuer\".\n- templates/session_detail.php + interactions inline : chevrons, modale de\n  garde-fou, snapshot FormData a l'ouverture pour detecter les\n  modifications, ecran de sauvegarde.\n- CSS maquette (.rv-modal__nav, .rv-modal--sm, .rv-saving) portee dans\n  public/assets + References/design.\n- i18n FR/EN, doc utilisateur (docs/sessions.md), TECHNICAL.md.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-616 - Sessions - Modale d'edition : navigation seance precedente/suivante + garde-fou modificat",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sessions.md",
        "public/assets/css/app.css",
        "src/Http/Sessions/EditModalTabs.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/session_detail.php",
        "tests/Http/Sessions/EditModalTabsTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 72,
          "deletions": 0,
          "lines_changed": 72
        },
        {
          "path": "docs/sessions.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "src/Http/Sessions/EditModalTabs.php",
          "insertions": 51,
          "deletions": 0,
          "lines_changed": 51
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "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/session_detail.php",
          "insertions": 178,
          "deletions": 1,
          "lines_changed": 179
        },
        {
          "path": "tests/Http/Sessions/EditModalTabsTest.php",
          "insertions": 52,
          "deletions": 0,
          "lines_changed": 52
        }
      ],
      "files_changed": 11,
      "insertions": 467,
      "deletions": 1
    },
    {
      "hash": "1ecac9d654efe3bcabe0b1d2939770ed3e314026",
      "short_hash": "1ecac9d",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787958774,
      "message": "SPOR-615 - Sessions - Import : archives ZIP (une ou plusieurs), extraction des seuls GPX/TCX/FIT\n\nSuite de SPOR-524. L'ajout de seance accepte des .zip (seul ou en lot,\nmelangeable a des fichiers nus). Le ZIP est etendu cote serveur ; seuls les\n.gpx/.tcx/.fit en sont extraits — tout le reste n'est jamais decompresse.\n\n- Domain\\Import\\ZipContents : lecteur ZIP PUR (zero dependance, gzinflate\n  seulement — ext-zip absente/non garantie chez OVH, cf. gd). Lit le central\n  directory, plafonds anti zip-bomb AVANT decompression (16 Mo/entree,\n  64 Mo total, ratio 1000:1, 50 entrees). Rejets d'entree : dossier, symlink\n  (S_IFLNK), chiffree, methode != stored/deflate, zip64, AppleDouble, CRC32\n  KO, extension non supportee (archives imbriquees incluses). Zip-slip :\n  basename() seul. findEocd() resilient a une fausse signature dans le\n  commentaire.\n- Http\\Sessions\\ZipStagingStore : multi-slot, token opaque, one-shot, TTL 1 h,\n  var/cache/zip-staging/.\n- SessionsController : import() detecte .zip -> importZip(). Corps d'import\n  d'un fichier extrait dans ingestFileAtPath() (partage upload direct /\n  reprise stagee). Mode lot -> stage + JSON {staged,ignored} ; nouvelle route\n  POST /sessions/import/staged -> importStaged() rejoue le pipeline complet\n  (doublon SPOR-199, choix de sport, pause/reprise SPOR-524). Sans JS ->\n  importZipQuietly() (import-all best-effort + flash resume).\n- interactions.js : initImportBatch() intercepte des qu'un .zip est\n  selectionne ; expandSelection() pre-passe, puis file d'attente a plat\n  (jobs file|staged|ignored).\n- session_modals.php accept=\".tcx,.gpx,.fit,.zip\" ; sessions.dropzone_hint FR/EN.\n\nTests : ZipContentsTest (14 cas, fixtures ZIP construites a la main sans\nZipArchive), ZipStagingStoreTest (5). Suite complete 1919 tests, 5244\nassertions, verte. Verifie en live (session forgee) : expansion, import\nstage, doublon via chemin stage, one-shot, POST non-batch.\n\nTECHNICAL.md + docs/sessions.md a jour. Mindmap 🟢. Jira SPOR-615 Done,\nsprint courant.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-615 - Sessions - Import : archives ZIP (une ou plusieurs), extraction des seuls GPX/TCX/FIT",
      "type": "ops",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/sessions.md",
        "public/assets/js/interactions.js",
        "src/Domain/Import/ZipContents.php",
        "src/Domain/Import/ZipException.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Http/Sessions/ZipStagingStore.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/session_modals.php",
        "tests/Domain/Import/ZipContentsTest.php",
        "tests/Http/Sessions/ZipStagingStoreTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 113,
          "deletions": 0,
          "lines_changed": 113
        },
        {
          "path": "config/routes.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "docs/sessions.md",
          "insertions": 13,
          "deletions": 1,
          "lines_changed": 14
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 80,
          "deletions": 10,
          "lines_changed": 90
        },
        {
          "path": "src/Domain/Import/ZipContents.php",
          "insertions": 273,
          "deletions": 0,
          "lines_changed": 273
        },
        {
          "path": "src/Domain/Import/ZipException.php",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 198,
          "deletions": 10,
          "lines_changed": 208
        },
        {
          "path": "src/Http/Sessions/ZipStagingStore.php",
          "insertions": 117,
          "deletions": 0,
          "lines_changed": 117
        },
        {
          "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/partials/session_modals.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "tests/Domain/Import/ZipContentsTest.php",
          "insertions": 239,
          "deletions": 0,
          "lines_changed": 239
        },
        {
          "path": "tests/Http/Sessions/ZipStagingStoreTest.php",
          "insertions": 94,
          "deletions": 0,
          "lines_changed": 94
        }
      ],
      "files_changed": 14,
      "insertions": 1153,
      "deletions": 24,
      "tags": [
        "v1.3.6"
      ]
    },
    {
      "hash": "eee774b0ca4a0b60b5c5db1ac6647a501b6e3627",
      "short_hash": "eee774b",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787957255,
      "message": "SPOR-614 - CI/CD - Deploy : actions/checkout@v4 -> @v5 (runtime Node 24)\n\nactions/checkout@v4 cible Node 20 (deprecie, force sur Node 24 par GitHub\navec une annotation a chaque run). @v5 cible Node 24 nativement. Une seule\nligne. shivammathur/setup-php@v2 non concerne (tag maintenu cote Node 24).\n\nCI-only, aucun code applicatif ni comportement de deploiement modifie.\nVerif au prochain deploiement (annotation absente). TECHNICAL.md a jour.\nMindmap 🟢. Jira SPOR-614 Done, sprint courant.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-614 - CI/CD - Deploy : actions/checkout@v4 -> @v5 (runtime Node 24)",
      "type": "ops",
      "modified_files": [
        ".github/workflows/deploy.yml",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        }
      ],
      "files_changed": 3,
      "insertions": 15,
      "deletions": 1
    },
    {
      "hash": "d3faf00c38a6e9b7b61c937092fab7f0367436a7",
      "short_hash": "d3faf00",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787956796,
      "message": "SPOR-613 - i18n - Page Citations communautaires (/quotes) FR/EN\n\nLa page /quotes (SPOR-501/530) n'avait jamais ete couverte par l'epic i18n\nSPOR-531 ; SPOR-536 n'avait traite que le texte bilingue des citations.\nTraduction COMPLETE (meme barre que SPOR-609/610) : chrome (topbar, 4\nonglets + badges, eyebrow, etats vides, cartes publiques/privees/ignorees,\n3 modales), flash, erreurs de validation, dates, libelles de visibilite.\n\n- Domain pur : QuoteInput::parse() renvoie des CODES d'erreur\n  (text_required/text_too_long/author_too_long/visibility_required) au lieu\n  de phrases anglaises. QuotesController::errorMessages() mappe vers\n  quotes.err_* via Translator (:max interpole depuis les constantes du\n  domaine). Meme approche que PainInjuriesController (SPOR-609).\n- QuotesController : $locale = Locale::current() une fois dans render(),\n  threade aux 3 presenters ; dateLabel() -> DateLabels::monthDayYear (en\n  'en' : rendu identique a M j, Y) ; tous les Flash::add via Translator\n  (quotes.flash_*, common.session_expired reutilise).\n- templates/quotes.php + quote_public_card.php + quote_private_edit_modal.php\n  cables sur Translator. Le texte des citations reste resolu par\n  LocalizedQuoteText (SPOR-536).\n- Nouveau bloc top-level quotes.* (en/fr), distinct de admin.quotes.*.\n\nTests : QuotesCatalogTest (61 cas : chaque cle FR != EN, interpolations,\nmapping des codes QuoteInput) ; QuoteInputTest reecrit (codes). Suite\ncomplete 1900 tests, 5205 assertions, verte. Verifie en live (session\nforgee) fr + en, GET + POST invalide.\n\nTECHNICAL.md + docs/quotes.md a jour. Mindmap 🟢. Jira SPOR-613 Done,\nsprint courant.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-613 - i18n - Page Citations communautaires (/quotes) FR/EN",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/quotes.md",
        "src/Domain/Content/QuoteInput.php",
        "src/Http/Content/QuotesController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/quote_private_edit_modal.php",
        "templates/partials/quote_public_card.php",
        "templates/quotes.php",
        "tests/Domain/QuoteInputTest.php",
        "tests/Infra/I18n/QuotesCatalogTest.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/quotes.md",
          "insertions": 27,
          "deletions": 20,
          "lines_changed": 47
        },
        {
          "path": "src/Domain/Content/QuoteInput.php",
          "insertions": 9,
          "deletions": 4,
          "lines_changed": 13
        },
        {
          "path": "src/Http/Content/QuotesController.php",
          "insertions": 64,
          "deletions": 37,
          "lines_changed": 101
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 63,
          "deletions": 0,
          "lines_changed": 63
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 62,
          "deletions": 0,
          "lines_changed": 62
        },
        {
          "path": "templates/partials/quote_private_edit_modal.php",
          "insertions": 18,
          "deletions": 15,
          "lines_changed": 33
        },
        {
          "path": "templates/partials/quote_public_card.php",
          "insertions": 8,
          "deletions": 4,
          "lines_changed": 12
        },
        {
          "path": "templates/quotes.php",
          "insertions": 40,
          "deletions": 35,
          "lines_changed": 75
        },
        {
          "path": "tests/Domain/QuoteInputTest.php",
          "insertions": 10,
          "deletions": 5,
          "lines_changed": 15
        },
        {
          "path": "tests/Infra/I18n/QuotesCatalogTest.php",
          "insertions": 134,
          "deletions": 0,
          "lines_changed": 134
        }
      ],
      "files_changed": 12,
      "insertions": 506,
      "deletions": 120,
      "tags": [
        "v1.3.5"
      ]
    },
    {
      "hash": "a0cb72dc0621fcae1f1c102e475d2aad85d1a14a",
      "short_hash": "a0cb72d",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787955849,
      "message": "SPOR-612 - Timeline - Le panneau de frise remplit la hauteur disponible\n\nLe panneau scrollable .rv-timeline-scroll etait plafonne a max-height:560px\n(valeur arbitraire de la 1re version, epic SPOR-527) : bande vide sous la\nfrise sur un ecran haut, scrollbar interne minuscule.\n\nCSS seul (components.css) : .rv-screen est deja une flex-column bornee en\nhauteur (.rv-app__body overflow:hidden, topbar exclue). Le panneau passe a\nflex:1 1 0 + min-height:0 (selecteur enfant-direct pour l'emporter sur\n.rv-screen > * { flex-shrink:0 }) et garde overflow-y:auto -> la hauteur\nvient de l'espace libre, le contenu long defile DANS le panneau sans pousser\n.rv-screen. .rv-timeline-central reste en hauteur de contenu.\n\nPas dans la maquette (Timeline globale = structure hors portage). Aucun\nchangement de balisage ni de JS. Verifie /timeline 4 echelles fr+en -> HTTP\n200 ; rendu visuel a valider cote utilisateur (pas de navigateur headless\nen local). Suite complete 1839 tests verte.\n\nTECHNICAL.md a jour (+ note de suivi du fix $locale 6bb1c91 sous SPOR-611).\nMindmap 🟢. Jira SPOR-612 Done, sprint courant.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-612 - Timeline - Le panneau de frise remplit la hauteur disponible",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/css/components.css"
      ],
      "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/css/components.css",
          "insertions": 8,
          "deletions": 1,
          "lines_changed": 9
        }
      ],
      "files_changed": 3,
      "insertions": 50,
      "deletions": 1
    },
    {
      "hash": "6bb1c919f5bbc52d4cd3444652fd2ea3380a7f1b",
      "short_hash": "6bb1c91",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787955505,
      "message": "SPOR-611 - Timeline - Fix : $locale non passe a sessionEvents() (TypeError sur /timeline)\n\nLe commit f1f9a84 avait etendu healthMeasureEvents()/medicalDocumentEvents()\npour recevoir $locale mais avait oublie sessionEvents(), qui appelle\nsessionMeta($row, $locale) depuis SPOR-611 -> \"Undefined variable $locale\"\npuis TypeError, /timeline HTTP 500 pour tout compte ayant au moins une\nseance.\n\nsessionEvents(int $userId, string $locale) + passage de $locale au site\nd'appel dans show(). Aucune autre methode d'evenement concernee.\n\nVerifie en live (session forgee, user 1) : /timeline?scale=day|week|month|\nyear en fr et en -> HTTP 200, zero Warning/Notice ; en-tetes FR localises\n(lundi 15 juin 2026 / Semaine du 29 juin au 5 juil. 2026 / juillet 2026),\nen inchange. Suite complete 1839 tests, 5017 assertions, verte.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-611 - Timeline - Fix : $locale non passe a sessionEvents() (TypeError sur /timeline)",
      "type": "fix",
      "modified_files": [
        "src/Http/Timeline/TimelineController.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Http/Timeline/TimelineController.php",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        }
      ],
      "files_changed": 1,
      "insertions": 2,
      "deletions": 2
    },
    {
      "hash": "f1f9a8443e4df8770f3ced94d1085f6edec84b9a",
      "short_hash": "f1f9a84",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787955087,
      "message": "SPOR-611 - i18n - Timeline : en-tetes de date + libelles de mesure FR/EN\n\nRevient sur la reserve de SPOR-552 (dates laissees en anglais).\n\n- Domain reste pur : TimelineScale::heading() garde le libelle anglais\n  (contrat teste) ; Timeline::build() expose headingDate (evenement le\n  plus recent du groupe). Nouveau Http\\Timeline\\TimelineHeadingPresenter\n  localise via Translator (timeline.heading_today/_yesterday/_week) +\n  DateLabels. Le controleur post-traite les groupes apres build().\n  En anglais : rendu identique a l'ancien.\n- healthMeasureEvents : HealthMeasureType::label() -> HealthVocab::\n  measureTypeLabel() (SPOR-610). ->label() reste la source de /trends\n  /calendar /events, non traduits.\n- sessionMeta() + meta document medical : ucfirst(sport) -> sportLabel()\n  (Translator::t('sport.*') + repli ucfirst pour sports personnalises).\n\nTests : TimelineHeadingPresenterTest (Today/Yesterday, date complete,\nplage de semaine, mois/annee, en+fr) ; TimelineTest assert headingDate.\nSuite complete 1839 tests, 5017 assertions, verte. Verifie en live\n(session forgee) 4 echelles fr + non-regression en.\n\nTECHNICAL.md + docs/timeline.md a jour. Mindmap 🟢. Jira SPOR-611 Done,\nsprint courant.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-611 - i18n - Timeline : en-tetes de date + libelles de mesure FR/EN",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/timeline.md",
        "src/Domain/Training/Timeline.php",
        "src/Http/Timeline/TimelineController.php",
        "src/Http/Timeline/TimelineHeadingPresenter.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/timeline.php",
        "tests/Domain/TimelineTest.php",
        "tests/Http/Timeline/TimelineHeadingPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 52,
          "deletions": 0,
          "lines_changed": 52
        },
        {
          "path": "docs/timeline.md",
          "insertions": 4,
          "deletions": 4,
          "lines_changed": 8
        },
        {
          "path": "src/Domain/Training/Timeline.php",
          "insertions": 9,
          "deletions": 5,
          "lines_changed": 14
        },
        {
          "path": "src/Http/Timeline/TimelineController.php",
          "insertions": 29,
          "deletions": 9,
          "lines_changed": 38
        },
        {
          "path": "src/Http/Timeline/TimelineHeadingPresenter.php",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        },
        {
          "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/timeline.php",
          "insertions": 11,
          "deletions": 9,
          "lines_changed": 20
        },
        {
          "path": "tests/Domain/TimelineTest.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "tests/Http/Timeline/TimelineHeadingPresenterTest.php",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        }
      ],
      "files_changed": 11,
      "insertions": 242,
      "deletions": 27
    },
    {
      "hash": "ae5335934402b875412409f3ab0d02b02d5022b0",
      "short_hash": "ae53359",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787954352,
      "message": "SPOR-610 - i18n - Page Physiologie (/health) FR/EN\n\nTraduction complete de /health (chrome, flash, erreurs de validation,\ndates) sur le meme modele que SPOR-609 :\n\n- Nouveau bloc health.* (en/fr) ; HealthVocab etendu (measureTypeLabel,\n  measureSecondaryLabel, bloodPressureCategoryLabel, reminderBadgeLabel)\n  -- les enums Domain restent purs, valeurs canoniques anglaises conservees.\n- HealthMeasureType::label() (domaine) inchange : /trends /calendar /events\n  /timeline partagent ce vocabulaire et gardent l'anglais jusqu'a leur\n  propre story (divergence documentee).\n- HealthController : $locale threade dans render() et les helpers ;\n  MeasureRowPresenter::present() prend un 2e param $locale = 'en' (defaut\n  = non-regression pour son test).\n- templates/health.php + reminder_form_fields.php cables sur Translator ;\n  script inline Systolic/Value injecte via json_encode(Translator::t()).\n- Pas de body-map sur /health -> rien a faire cote interactions.js.\n\nTests : HealthCatalogTest + HealthVocabTest etendu. Suite complete\n1834 tests, 4994 assertions, verte. Verifie en live (session forgee)\nfr + en + non-regression /trends /calendar /events /timeline.\n\nTECHNICAL.md + docs/sante.md a jour. Mindmap 🟢. Jira SPOR-610 Done,\nsprint courant.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-610 - i18n - Page Physiologie (/health) FR/EN",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sante.md",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/MeasureRowPresenter.php",
        "src/Infra/I18n/HealthVocab.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/health.php",
        "templates/partials/reminder_form_fields.php",
        "tests/Infra/I18n/HealthCatalogTest.php",
        "tests/Infra/I18n/HealthVocabTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 73,
          "deletions": 0,
          "lines_changed": 73
        },
        {
          "path": "docs/sante.md",
          "insertions": 22,
          "deletions": 14,
          "lines_changed": 36
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 77,
          "deletions": 65,
          "lines_changed": 142
        },
        {
          "path": "src/Http/Health/MeasureRowPresenter.php",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "src/Infra/I18n/HealthVocab.php",
          "insertions": 42,
          "deletions": 1,
          "lines_changed": 43
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 130,
          "deletions": 0,
          "lines_changed": 130
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 130,
          "deletions": 0,
          "lines_changed": 130
        },
        {
          "path": "templates/health.php",
          "insertions": 82,
          "deletions": 76,
          "lines_changed": 158
        },
        {
          "path": "templates/partials/reminder_form_fields.php",
          "insertions": 19,
          "deletions": 15,
          "lines_changed": 34
        },
        {
          "path": "tests/Infra/I18n/HealthCatalogTest.php",
          "insertions": 161,
          "deletions": 0,
          "lines_changed": 161
        },
        {
          "path": "tests/Infra/I18n/HealthVocabTest.php",
          "insertions": 29,
          "deletions": 0,
          "lines_changed": 29
        }
      ],
      "files_changed": 12,
      "insertions": 770,
      "deletions": 173
    },
    {
      "hash": "dbe71978e021d8089ab672800dbc8133ab2e85c4",
      "short_hash": "dbe7197",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787953142,
      "message": "SPOR-609 - i18n - Page Pain & Injuries (/pain-injuries) FR/EN\n\nTraduction complete de /pain-injuries (chrome, flash, erreurs de validation,\ndates) + descripteurs Domain via un presenter de presentation Infra\\I18n\\\nHealthVocab (Domain reste pur) : BodyRegion, InjuryEvolutionStatus,\nSessionPainMark tissue/sensation.\n\nBody-map partage (interactions.js) : lit window.RvBodyMapI18n (nouveau\npartial bodymap_i18n.php) avec repli anglais embarque -> zero regression\nsur /health et /sessions. Chips Tissue/Sensation : data-chip-value\ncanonique, aucun changement JS pour la valeur envoyee.\n\nGraphiques (pain-load-chart.js, injury-timeline-chart.js) : chaines + mois\ncourts via le payload data-chart (plus de locale navigateur).\n\nCatalogue : blocs pain_injuries.* et body_region.* (en/fr) ; hero_summary\ncombine deux cles plurielles ; reutilise common.* et nav.*.\n\nTests : PainInjuriesCatalogTest + HealthVocabTest (102 cas). Suite complete\n1741 tests verte. Verifie en live (session forgee) fr + en + non-regression\n/health /sessions.\n\nTECHNICAL.md + docs/pain-injuries.md a jour. Mindmap 🟢. Jira SPOR-609 Done,\nsprint courant.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-609 - i18n - Page Pain & Injuries (/pain-injuries) FR/EN",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/pain-injuries.md",
        "public/assets/js/injury-timeline-chart.js",
        "public/assets/js/interactions.js",
        "public/assets/js/pain-load-chart.js",
        "src/Http/Health/PainInjuriesController.php",
        "src/Infra/I18n/HealthVocab.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/pain_injuries.php",
        "templates/partials/bodymap_i18n.php",
        "templates/partials/injury_form_fields.php",
        "tests/Infra/I18n/HealthVocabTest.php",
        "tests/Infra/I18n/PainInjuriesCatalogTest.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": "docs/pain-injuries.md",
          "insertions": 46,
          "deletions": 34,
          "lines_changed": 80
        },
        {
          "path": "public/assets/js/injury-timeline-chart.js",
          "insertions": 14,
          "deletions": 7,
          "lines_changed": 21
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 16,
          "deletions": 2,
          "lines_changed": 18
        },
        {
          "path": "public/assets/js/pain-load-chart.js",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "src/Http/Health/PainInjuriesController.php",
          "insertions": 73,
          "deletions": 51,
          "lines_changed": 124
        },
        {
          "path": "src/Infra/I18n/HealthVocab.php",
          "insertions": 83,
          "deletions": 0,
          "lines_changed": 83
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 158,
          "deletions": 0,
          "lines_changed": 158
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 162,
          "deletions": 0,
          "lines_changed": 162
        },
        {
          "path": "templates/pain_injuries.php",
          "insertions": 102,
          "deletions": 73,
          "lines_changed": 175
        },
        {
          "path": "templates/partials/bodymap_i18n.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "templates/partials/injury_form_fields.php",
          "insertions": 28,
          "deletions": 24,
          "lines_changed": 52
        },
        {
          "path": "tests/Infra/I18n/HealthVocabTest.php",
          "insertions": 69,
          "deletions": 0,
          "lines_changed": 69
        },
        {
          "path": "tests/Infra/I18n/PainInjuriesCatalogTest.php",
          "insertions": 203,
          "deletions": 0,
          "lines_changed": 203
        }
      ],
      "files_changed": 15,
      "insertions": 1106,
      "deletions": 193
    },
    {
      "hash": "77f36cb8307125f2ebe7feb2e822d42221b9139c",
      "short_hash": "77f36cb",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787950641,
      "message": "SPOR-606 - CI/CD - Deploy FTP : --ignore-time au lieu de --size-only (option lftp inexistante)\n\nLe deploy v1.3.4 a echoue : `mirror: unrecognized option '--size-only'` — cette\noption est celle de rsync, pas de lftp. L'equivalent lftp est `--ignore-time`\n(ignore la date, compare la taille), present depuis toujours.\n\n- .github/workflows/deploy.yml : mirror vendor/ passe de --size-only a\n  --ignore-time.\n- TECHNICAL.md : wording aligne.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-606 - CI/CD - Deploy FTP : --ignore-time au lieu de --size-only (option lftp inexistante)",
      "type": "ops",
      "modified_files": [
        ".github/workflows/deploy.yml",
        "TECHNICAL.md"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        }
      ],
      "files_changed": 2,
      "insertions": 3,
      "deletions": 3,
      "tags": [
        "v1.3.4"
      ]
    },
    {
      "hash": "e6399711a3fc64fe239627ee26b05d75de067f6c",
      "short_hash": "e639971",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787948854,
      "message": "SPOR-608 - Evenements - Onglet Physiologie : sparkline par mesure + colonnes Debut/Fin lisibles\n\nDemande de l'utilisateur : \"fait des sparklines pour chaque type de mesure et\nrevoit les colonnes Debut et Fin avec date et valeur, ce n'est pas lisible\".\n\n- EventPhysiologySummary::forWindow() : nouvelle cle `series` par mesure\n  (valeurs des lectures prises en compte, ORDRE CHRONOLOGIQUE : usort du\n  statSet ajoute). Aucune requete en plus.\n- EventPhysiologyPresenter::forRange() : expose `series` + `type` (clef couleur).\n- event_detail.php :\n  * colonne \"Tendance\" (events.physio_col_trend FR/EN) = <canvas\n    class=\"rv-sparkline\"> — meme composant que /health, initialise par\n    health-trends.js ajoute a la page. \"--\" si < 2 lectures.\n  * couleur de sparkline distincte par type ($physioSpark), alignee sur\n    health.php pour Weight/Resting HR.\n  * colonnes Debut/Fin : valeur en gras puis date en dessous (rv-eyebrow),\n    au lieu de \"valeur date\" colles. nowrap sur Variation / Min-Max.\n- EventPhysiologySummaryTest : series chronologique, lecture unique. phpunit\n  1639 verts.\n- TECHNICAL.md + docs/events.md + mindmap ; SPOR-608 -> Done, sprint courant.\n- Verifie : /events/2 -> sparklines Weight (teal) et Body fat (fox), \"--\" pour\n  les mesures a une seule lecture, dates empilees sous les valeurs.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-608 - Evenements - Onglet Physiologie : sparkline par mesure + colonnes Debut/Fin lisibles",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/events.md",
        "src/Domain/Health/EventPhysiologySummary.php",
        "src/Http/Events/EventPhysiologyPresenter.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/event_detail.php",
        "tests/Domain/EventPhysiologySummaryTest.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/events.md",
          "insertions": 6,
          "deletions": 4,
          "lines_changed": 10
        },
        {
          "path": "src/Domain/Health/EventPhysiologySummary.php",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        },
        {
          "path": "src/Http/Events/EventPhysiologyPresenter.php",
          "insertions": 5,
          "deletions": 2,
          "lines_changed": 7
        },
        {
          "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": "templates/event_detail.php",
          "insertions": 48,
          "deletions": 12,
          "lines_changed": 60
        },
        {
          "path": "tests/Domain/EventPhysiologySummaryTest.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        }
      ],
      "files_changed": 9,
      "insertions": 142,
      "deletions": 19
    },
    {
      "hash": "42c2a9ac4ee9bb3675b2a105140a04455ed26065",
      "short_hash": "42c2a9a",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787947949,
      "message": "SPOR-607 - Evenements - Onglet Roles en camembert colore (au lieu d'une liste)\n\nDemande de l'utilisateur : l'onglet Roles de /events/{id} devrait etre une\nrepresentation graphique -> un camembert avec une couleur differente par tag.\n\n- Domain/Training/EventRoleBreakdown::fromSessionTags() : compte de seances\n  par label de tag, ordre count desc puis label asc (pur, teste). Logique\n  jusqu'ici inline dans le template (non testee).\n- EventsController::detail() : $tagsBySession calcule une fois, passe au\n  template ET a EventRoleBreakdown -> cle roleBreakdown.\n- event_detail.php : panneau roles = <canvas id=\"event-roles-chart\"\n  data-chart=\"{labels,counts}\"> (h 260px) au lieu du rv-stack de badges.\n  Etat vide inchange.\n- public/assets/js/event-roles-chart.js (nouveau, calque event-load-chart.js) :\n  type 'pie' (Chart.js 4.5.1 vendorise), une couleur par part (palette aurora\n  8 teintes de tokens.css, recyclee au-dela), legende a droite, tooltip\n  \"libelle : N session(s) (P%)\". P% relatif au total des roles attribues\n  (une seance peut porter plusieurs roles).\n- EventRoleBreakdownTest ; phpunit 1637 verts.\n- TECHNICAL.md + docs/events.md + mindmap ; SPOR-607 -> Done, sprint courant.\n- Verifie : /events/2 rend le canvas avec data-chart correct, plus de liste.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-607 - Evenements - Onglet Roles en camembert colore (au lieu d'une liste)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/events.md",
        "public/assets/js/event-roles-chart.js",
        "src/Domain/Training/EventRoleBreakdown.php",
        "src/Http/Events/EventsController.php",
        "templates/event_detail.php",
        "tests/Domain/EventRoleBreakdownTest.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": "docs/events.md",
          "insertions": 7,
          "deletions": 4,
          "lines_changed": 11
        },
        {
          "path": "public/assets/js/event-roles-chart.js",
          "insertions": 71,
          "deletions": 0,
          "lines_changed": 71
        },
        {
          "path": "src/Domain/Training/EventRoleBreakdown.php",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "src/Http/Events/EventsController.php",
          "insertions": 7,
          "deletions": 1,
          "lines_changed": 8
        },
        {
          "path": "templates/event_detail.php",
          "insertions": 22,
          "deletions": 18,
          "lines_changed": 40
        },
        {
          "path": "tests/Domain/EventRoleBreakdownTest.php",
          "insertions": 56,
          "deletions": 0,
          "lines_changed": 56
        }
      ],
      "files_changed": 8,
      "insertions": 264,
      "deletions": 23
    },
    {
      "hash": "ce87cef10fd93a6a36261fd2a1b15c17d79aae92",
      "short_hash": "ce87cef",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787947393,
      "message": "SPOR-606 - CI/CD - Deploy FTP : mirror vendor/ en --size-only (stop au re-transfert integral)\n\nSuite de SPOR-606 : le mirror re-televersait TOUT vendor/ (~10 000 fichiers)\na chaque deploy car `composer install` regenere tous les mtimes -> la\ncomparaison par date voit tout comme modifie (lent + fragile).\n\n- deploy.yml : deux `mirror` maintenant. vendor/ d'abord en --size-only (le\n  contenu d'une lib ne change que si composer.lock change, et sa taille\n  change alors). Puis le code applicatif avec --exclude-glob vendor/,\n  comparaison par date conservee (modif de meme taille toujours detectee).\n- TECHNICAL.md : addendum SPOR-606 mis a jour (2 causes, 2 mirror).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-606 - CI/CD - Deploy FTP : mirror vendor/ en --size-only (stop au re-transfert integral)",
      "type": "ops",
      "modified_files": [
        ".github/workflows/deploy.yml",
        "TECHNICAL.md"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 27,
          "deletions": 19,
          "lines_changed": 46
        }
      ],
      "files_changed": 2,
      "insertions": 39,
      "deletions": 19
    },
    {
      "hash": "1e3ede0fc9e3d815be5afa8bd07bd4022a1dc40a",
      "short_hash": "1e3ede0",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787946901,
      "message": "SPOR-606 - CI/CD - Deploy FTP : exclure les dossiers de cache serveur du mirror --delete\n\nLe deploy de v1.3.3 a echoue a l'etape FTP : `lftp mirror --reverse --delete`\ntentait de supprimer ./tiles a la racine du DocumentRoot (vieux dossier de\ncache, non vide) -> OVH renvoie 550 Directory not empty -> `cmd:fail-exit yes`\ninterrompt tout le mirror. Idem var/cache/tiles/ (cache de public/tiles.php)\nefface a chaque deploy.\n\n- .github/workflows/deploy.yml : `--exclude-glob var/cache/` et\n  `--exclude-glob tiles/` ajoutes aux exclusions du mirror (comme var/log/,\n  var/seed/, logs/). Ces dossiers sont du runtime, jamais dans dist/.\n- net:max-retries 2 -> 4, net:timeout 20 -> 25, reconnect-interval-base 5 :\n  le re-transfert quasi-complet de vendor/ a chaque build est long et fragile.\n- TECHNICAL.md (addendum CI/CD) + mindmap ; SPOR-606 -> Done, sprint courant.\n- Action manuelle a faire : supprimer ./tiles (racine) du serveur par FTP.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-606 - CI/CD - Deploy FTP : exclure les dossiers de cache serveur du mirror --delete",
      "type": "ops",
      "modified_files": [
        ".github/workflows/deploy.yml",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 10,
          "deletions": 1,
          "lines_changed": 11
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        }
      ],
      "files_changed": 3,
      "insertions": 44,
      "deletions": 1
    },
    {
      "hash": "14260b4752bff4cd1bae346c5670860a8c8ff01c",
      "short_hash": "14260b4",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787946032,
      "message": "SPOR-605 - Evenements - Ruban \"En cours\" (couleur distincte) pour un evenement commence\n\nDemande de l'utilisateur : \"lorsque l'event est en cours, change le ruban en\n'En cours' avec une autre couleur\".\n\n- EventDateRange : le booleen $planned devient $phase (PHASE_PAST /\n  PHASE_ONGOING / PHASE_UPCOMING), calcule sur la plage etendue (seances U\n  courses rattachees) vs aujourd'hui.\n  * debut dans le futur -> upcoming -> ruban/badge \"Planifie\" (vert, inchange)\n  * aujourd'hui dans la plage -> ongoing -> ruban/badge \"En cours\" (bleu,\n    --status-info) + icone activity\n  * fin passee / aucune date -> past -> aucun ruban\n- events.php (rv-corner-ribbon--ongoing) + event_detail.php (badge en-tete) +\n  app.css (--ongoing mutualise avec --considering) + i18n events.ribbon_ongoing.\n- EventDateRangeTest reecrit sur $phase (8 cas). phpunit 1633 verts.\n- TECHNICAL.md + docs/events.md + mindmap ; SPOR-605 -> Done, sprint courant.\n- Verifie : course future rattachee a un evenement a seances passees -> ruban\n  \"En cours\" bleu sur /events et badge sur la fiche ; /events/13 (course a\n  venir) reste \"Planifie\".\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-605 - Evenements - Ruban \"En cours\" (couleur distincte) pour un evenement commence",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/events.md",
        "public/assets/css/app.css",
        "src/Domain/Training/EventDateRange.php",
        "src/Http/Events/EventsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/event_detail.php",
        "templates/events.php",
        "tests/Domain/EventDateRangeTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "docs/events.md",
          "insertions": 13,
          "deletions": 6,
          "lines_changed": 19
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Domain/Training/EventDateRange.php",
          "insertions": 32,
          "deletions": 12,
          "lines_changed": 44
        },
        {
          "path": "src/Http/Events/EventsController.php",
          "insertions": 7,
          "deletions": 5,
          "lines_changed": 12
        },
        {
          "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": "templates/event_detail.php",
          "insertions": 11,
          "deletions": 4,
          "lines_changed": 15
        },
        {
          "path": "templates/events.php",
          "insertions": 11,
          "deletions": 5,
          "lines_changed": 16
        },
        {
          "path": "tests/Domain/EventDateRangeTest.php",
          "insertions": 28,
          "deletions": 12,
          "lines_changed": 40
        }
      ],
      "files_changed": 11,
      "insertions": 150,
      "deletions": 45,
      "tags": [
        "v1.3.3"
      ]
    },
    {
      "hash": "62e335a656c37756dc522dc789d3da2ac54f30cb",
      "short_hash": "62e335a",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787945534,
      "message": "SPOR-604 - Evenements - Onglet Physiologie : plage etendue + evenement planifie\n\nBug remonte par l'utilisateur (/events/13, evenement course-seule) : onglet\nPhysiologie vide.\n\n- EventPhysiologyPresenter::forSessions() -> forRange(EventDateRange) : la\n  fenetre suit la plage ETENDUE de l'evenement (seances U courses rattachees,\n  SPOR-600), comme l'en-tete / le ruban / le calendrier. [] uniquement si\n  l'evenement n'a ni seance ni course.\n- EventPhysiologySummary::forWindow() : une seule lecture exploitable suffit\n  (dans la plage, ou ancre avant, ou ancre apres). Un evenement planifie / en\n  cours (course a venir, pas encore d'ancre \"apres\") affiche l'etat recent\n  sans delta (\"--\" en variation) au lieu de rien. Reste borne a +/- 7 jours.\n- Tests : cas \"ancre avant seule -> omis\" devient \"-> etat recent sans delta\"\n  (+ symetrique). phpunit 1631 verts.\n- TECHNICAL.md + docs/events.md + mindmap ; SPOR-604 -> Done, sprint courant.\n- Verifie : /events/13 affiche desormais Weight/Resting HR/HRV/SpO2/Body fat\n  (etat au 24-28/08, course le 30/08), colonne Variation \"--\", note d'encadrement.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-604 - Evenements - Onglet Physiologie : plage etendue + evenement planifie",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/events.md",
        "src/Domain/Health/EventPhysiologySummary.php",
        "src/Http/Events/EventPhysiologyPresenter.php",
        "src/Http/Events/EventsController.php",
        "templates/event_detail.php",
        "tests/Domain/EventPhysiologySummaryTest.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": "docs/events.md",
          "insertions": 9,
          "deletions": 2,
          "lines_changed": 11
        },
        {
          "path": "src/Domain/Health/EventPhysiologySummary.php",
          "insertions": 16,
          "deletions": 5,
          "lines_changed": 21
        },
        {
          "path": "src/Http/Events/EventPhysiologyPresenter.php",
          "insertions": 13,
          "deletions": 8,
          "lines_changed": 21
        },
        {
          "path": "src/Http/Events/EventsController.php",
          "insertions": 5,
          "deletions": 4,
          "lines_changed": 9
        },
        {
          "path": "templates/event_detail.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "tests/Domain/EventPhysiologySummaryTest.php",
          "insertions": 28,
          "deletions": 4,
          "lines_changed": 32
        }
      ],
      "files_changed": 8,
      "insertions": 124,
      "deletions": 25
    },
    {
      "hash": "fddd0467363171cd8cfd18e68e85a29ee042e7ca",
      "short_hash": "fddd046",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787941983,
      "message": "Sessions - Import FIT : fuseau ignore pour un parcours virtuel / home trainer (SPOR-603)\n\nBug remonte par l'utilisateur (/sessions/10180, /sessions/10181) : une sortie\nhome trainer (FIT sport=cycling, sub_sport=virtual_activity, Zwift & co) porte\ndes coordonnees GPS du monde virtuel (Watopia ~ iles Salomon, Londres\nvirtuelle...). SessionTimezoneResolver en deduisait le fuseau -> started_at\ndecale de plusieurs heures (ex. 18:27 CEST stocke 12:27 EDT).\n\n- Domain/Training/IndoorActivity::isLocationless() : home_trainer + treadmill\n  -> trace GPS eventuelle virtuelle, jamais utilisee pour le fuseau (pur, teste)\n- SessionTimezoneResolver::resolve() : parametre ?string $subsport ; la branche\n  GPS ne se declenche que si la sous-categorie n'est pas \"sans lieu\". Cable aux\n  3 appels (persistSession, applyMergedTrace, mergeFieldValuesFromImport).\n- FitImporter::mapSubSport() : [2,58] (cycling/virtual_activity) -> home_trainer\n  -> imports virtuels futurs corrects sans action utilisateur\n- Domain/Training/VirtualRideStartTime::correction() : heure corrigee + delta,\n  null si l'ecart < 60 s (troncature, pas le bug de fuseau) (pur, teste)\n- Infra/Db/SessionStartTimezoneBackfill + bin/ + public/ (ops_token) :\n  recalcule started_at (+ laps + segments decales) des seances virtuelles avec\n  GPS depuis l'heure UTC du blob -> fuseau du profil. Idempotent, saute les\n  seances dont started_at a ete edite a la main (field_overrides).\n- Coordonnees GPS virtuelles conservees (mini-carte) : hors perimetre.\n- TECHNICAL.md + docs/sessions.md + mindmap ; SPOR-603 -> Done, sprint courant.\n- Verifie : phpunit 1630 verts ; backfill local corrige 10180 (12:27->18:27:06)\n  et 10181 (17:42->18:42:51), 2e passage -> 0 ; /sessions/10180-10181 affichent\n  bien 18:27 / 18:42.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Sessions - Import FIT : fuseau ignore pour un parcours virtuel / home trainer (SPOR-603)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "bin/backfill_session_start_timezone.php",
        "docs/sessions.md",
        "public/backfill_session_start_timezone.php",
        "src/Domain/Import/FitImporter.php",
        "src/Domain/Training/IndoorActivity.php",
        "src/Domain/Training/VirtualRideStartTime.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionTimezoneResolver.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Infra/Db/SessionStartTimezoneBackfill.php",
        "tests/Domain/FitImporterTest.php",
        "tests/Domain/IndoorActivityTest.php",
        "tests/Domain/VirtualRideStartTimeTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 67,
          "deletions": 0,
          "lines_changed": 67
        },
        {
          "path": "bin/backfill_session_start_timezone.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "public/backfill_session_start_timezone.php",
          "insertions": 93,
          "deletions": 0,
          "lines_changed": 93
        },
        {
          "path": "src/Domain/Import/FitImporter.php",
          "insertions": 6,
          "deletions": 1,
          "lines_changed": 7
        },
        {
          "path": "src/Domain/Training/IndoorActivity.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "src/Domain/Training/VirtualRideStartTime.php",
          "insertions": 47,
          "deletions": 0,
          "lines_changed": 47
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 13,
          "deletions": 9,
          "lines_changed": 22
        },
        {
          "path": "src/Http/Sessions/SessionTimezoneResolver.php",
          "insertions": 9,
          "deletions": 2,
          "lines_changed": 11
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/Db/SessionStartTimezoneBackfill.php",
          "insertions": 164,
          "deletions": 0,
          "lines_changed": 164
        },
        {
          "path": "tests/Domain/FitImporterTest.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "tests/Domain/IndoorActivityTest.php",
          "insertions": 57,
          "deletions": 0,
          "lines_changed": 57
        },
        {
          "path": "tests/Domain/VirtualRideStartTimeTest.php",
          "insertions": 82,
          "deletions": 0,
          "lines_changed": 82
        }
      ],
      "files_changed": 15,
      "insertions": 616,
      "deletions": 15,
      "tags": [
        "v1.3.2"
      ]
    },
    {
      "hash": "731aa06c80d210445f5aa3b4a5d97e597c73944f",
      "short_hash": "731aa06",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787941964,
      "message": "Doc - CLAUDE.md : affectation au sprint courant des stories livrees dans la passe\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Doc - CLAUDE.md : affectation au sprint courant des stories livrees dans la passe",
      "type": "docs",
      "modified_files": [
        "CLAUDE.md"
      ],
      "modified_files_details": [
        {
          "path": "CLAUDE.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        }
      ],
      "files_changed": 1,
      "insertions": 6,
      "deletions": 0
    },
    {
      "hash": "83b0b7b915cefc270da728f90c645fde7bc182e6",
      "short_hash": "83b0b7b",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787939490,
      "message": "fix: Add guidance for commit",
      "message_short": "fix: Add guidance for commit",
      "type": "fix",
      "modified_files": [
        ".claude/settings.json",
        "CLAUDE.md"
      ],
      "modified_files_details": [
        {
          "path": ".claude/settings.json",
          "insertions": 0,
          "deletions": 1,
          "lines_changed": 1
        },
        {
          "path": "CLAUDE.md",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        }
      ],
      "files_changed": 2,
      "insertions": 5,
      "deletions": 1
    },
    {
      "hash": "4ce78e3e82d51d0bccf6bde2e00160552014ee14",
      "short_hash": "4ce78e3",
      "author": "zRenard",
      "email": "zRenard@users.noreply.github.com",
      "timestamp": 1787939430,
      "message": "Add Renovate configuration file",
      "message_short": "Add Renovate configuration file",
      "type": "feat",
      "modified_files": [
        "renovate.json"
      ],
      "modified_files_details": [
        {
          "path": "renovate.json",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        }
      ],
      "files_changed": 1,
      "insertions": 21,
      "deletions": 0
    },
    {
      "hash": "ab5dfafac3ee215bcbfb95d90c32c6bb005db2ab",
      "short_hash": "ab5dfaf",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787938717,
      "message": "Evenements - Image de course dans le carousel et la vignette de la vue reduite (SPOR-602)\n\nL'image de couverture d'une course (SPOR-511) est desormais traitee comme\nune image de l'Evenement, selon les 3 provenances de la section \"Races\"\n(rattachement explicite, course liee a une seance, course dans la plage de\ndates etendue) :\n\n- Fiche detail : le carousel (SPOR-575) ajoute l'image de chaque course qui\n  en a une, apres les photos de l'Evenement et des seances. Il s'affiche\n  meme si l'Evenement et ses seances n'ont aucune photo.\n- Vue reduite /events : l'image de course entre dans le pool du tirage\n  aleatoire de la vignette (SPOR-579).\n- Aucune route nouvelle : octets servis par /races/{id}/image.\n\n- Domain/Training/EventPhotoPool : fusion ordonnee + dedup d'URL (pur, teste)\n- Domain/Training/EventRacePhotoAssignment : assignation course->Evenement\n  en batch pour /events (pur, teste)\n- EventRacePicker : has_image dans forEvent() + coverPhotos() /\n  coverUrlsForEventList()\n- EventsController : linkedRaces calcule avant carouselPhotos() ; plage de\n  dates par Evenement calculee une fois et partagee avec randomPhotosByEvent()\n- TECHNICAL.md + docs/events.md + mindmap ; SPOR-602 -> Done\n- Verifie : phpunit 1615 verts + smoke HTTP (session forgee) /events et\n  /events/{id} avec course a image rattachee -> carousel et vignette OK\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Evenements - Image de course dans le carousel et la vignette de la vue reduite (SPOR-602)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/events.md",
        "src/Domain/Training/EventPhotoPool.php",
        "src/Domain/Training/EventRacePhotoAssignment.php",
        "src/Http/Events/EventRacePicker.php",
        "src/Http/Events/EventsController.php",
        "templates/event_detail.php",
        "tests/Domain/EventPhotoPoolTest.php",
        "tests/Domain/EventRacePhotoAssignmentTest.php"
      ],
      "modified_files_details": [
        {
          "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/events.md",
          "insertions": 9,
          "deletions": 5,
          "lines_changed": 14
        },
        {
          "path": "src/Domain/Training/EventPhotoPool.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "src/Domain/Training/EventRacePhotoAssignment.php",
          "insertions": 60,
          "deletions": 0,
          "lines_changed": 60
        },
        {
          "path": "src/Http/Events/EventRacePicker.php",
          "insertions": 79,
          "deletions": 2,
          "lines_changed": 81
        },
        {
          "path": "src/Http/Events/EventsController.php",
          "insertions": 63,
          "deletions": 28,
          "lines_changed": 91
        },
        {
          "path": "templates/event_detail.php",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "tests/Domain/EventPhotoPoolTest.php",
          "insertions": 54,
          "deletions": 0,
          "lines_changed": 54
        },
        {
          "path": "tests/Domain/EventRacePhotoAssignmentTest.php",
          "insertions": 107,
          "deletions": 0,
          "lines_changed": 107
        }
      ],
      "files_changed": 10,
      "insertions": 486,
      "deletions": 38,
      "tags": [
        "v1.3.1"
      ]
    },
    {
      "hash": "112d5b36b9cbcd61eb5601b24870ff5842051fb6",
      "short_hash": "112d5b3",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787933129,
      "message": "CI/CD - routes.php : instanciation paresseuse des controleurs (SPOR-601)\n\nLe deploy de v1.3.0 (SPOR-600) a echoue au 1er run sur /_ops/migrate\n(corps vide, < 1 s) : routes.php instanciait tous les controleurs au\nchargement du fichier, or juste apres l'upload FTP OPcache sert encore\nl'ancien autoload_classmap.php -> la classe du controleur tout juste\najoute n'est pas autoloadable -> fatal au bootstrap sur TOUTES les\nroutes, /_ops/* compris. Le re-run passe.\n\nHelper $lazy(fn () => new X(...)) : le new est differe au 1er dispatch\nd'une des routes du controleur (instance memoisee, une par requete).\nRoutes : [$ctrl, 'method'] -> $ctrl('method'). Une classe pas encore\nautoloadable n'affecte plus que ses propres routes.\n\nPiege : la closure-handler renvoyee ne doit pas etre `static` (Slim\nCallableResolver::bindToContainer fait un Closure::bind dessus).\n\n- config/routes.php : helper + passe sur ~200 lignes de routes\n- TECHNICAL.md : addendum sous CI/CD ; mindmap\n- Verifie : phpunit 1602 verts + smoke HTTP ~15 routes (GET 200 sans\n  warning, POST 302, /_ops/migrate sans token -> 403 Forbidden propre)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "CI/CD - routes.php : instanciation paresseuse des controleurs (SPOR-601)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 43,
          "deletions": 0,
          "lines_changed": 43
        },
        {
          "path": "config/routes.php",
          "insertions": 252,
          "deletions": 228,
          "lines_changed": 480
        }
      ],
      "files_changed": 3,
      "insertions": 296,
      "deletions": 228
    },
    {
      "hash": "4ef1939a7b6f4693d195c77430cba27a4a77d38f",
      "short_hash": "4ef1939",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787931051,
      "message": "Evenements - Courses planifiees : rattachement explicite + ruban \"Planifie\" (SPOR-600)\n\nUn Evenement peut desormais etre prepare a l'avance autour d'une course a\nvenir : la plage de dates integre les due_date des courses rattachees\n(goals.event_id, migration 128), et tant que la borne de fin est dans le\nfutur un ruban \"Planifie\" s'affiche sur la carte /events et en badge sur\nla fiche. La section Races de la fiche fusionne (dedup par id) : course\nliee via une seance (SPOR-576), course explicitement rattachee, et course\ndont la due_date tombe dans la plage. Boutons \"Add race\" / \"Detach\".\n\n- db/migrations/128_add_goals_event_id.sql : goals.event_id FK ON DELETE SET NULL\n- Domain\\Training\\EventDateRange (pur) + EventDateRangeTest (6 cas)\n- Http\\Events\\EventRacePicker (attach/detach/forEvent/attachableRaces)\n- Http\\Events\\EventRacesController + routes POST /events/{id}/races[/{raceId}/detach]\n- EventsController : plage etendue sur /events et la fiche ; linkedRaces()/\n  dateRangeLabel() obsoletes supprimees\n- templates/events.php + event_detail.php : ruban, badge, modale, section Races\n- i18n FR/EN : events.ribbon_planned / add_race_button / attach_race_label /\n  detach_race / races_empty\n- TECHNICAL.md + docs/events.md + mindmap\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Evenements - Courses planifiees : rattachement explicite + ruban \"Planifie\" (SPOR-600)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/128_add_goals_event_id.sql",
        "docs/events.md",
        "src/Domain/Training/EventDateRange.php",
        "src/Http/Events/EventRacePicker.php",
        "src/Http/Events/EventRacesController.php",
        "src/Http/Events/EventsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/event_detail.php",
        "templates/events.php",
        "tests/Domain/EventDateRangeTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 100,
          "deletions": 0,
          "lines_changed": 100
        },
        {
          "path": "config/routes.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "db/migrations/128_add_goals_event_id.sql",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "docs/events.md",
          "insertions": 39,
          "deletions": 6,
          "lines_changed": 45
        },
        {
          "path": "src/Domain/Training/EventDateRange.php",
          "insertions": 59,
          "deletions": 0,
          "lines_changed": 59
        },
        {
          "path": "src/Http/Events/EventRacePicker.php",
          "insertions": 182,
          "deletions": 0,
          "lines_changed": 182
        },
        {
          "path": "src/Http/Events/EventRacesController.php",
          "insertions": 87,
          "deletions": 0,
          "lines_changed": 87
        },
        {
          "path": "src/Http/Events/EventsController.php",
          "insertions": 48,
          "deletions": 61,
          "lines_changed": 109
        },
        {
          "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/event_detail.php",
          "insertions": 85,
          "deletions": 15,
          "lines_changed": 100
        },
        {
          "path": "templates/events.php",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        },
        {
          "path": "tests/Domain/EventDateRangeTest.php",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        }
      ],
      "files_changed": 14,
      "insertions": 730,
      "deletions": 83,
      "tags": [
        "v1.3.0"
      ]
    },
    {
      "hash": "e9a0924fe69b848d13d09e28d9cb442320df68d5",
      "short_hash": "e9a0924",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787924107,
      "message": "Doc - CLAUDE.md : tracabilite Jira dans le code livre + regle commit/tag\n\nToute classe/methode/bloc livre pour une story porte la reference SPOR-XXX\nen commentaire (docblock ou au plus pres du code), idem pour les tests —\nretrouver la story depuis le code sans git blame. Formalise aussi la regle\ndeja appliquee : demander a l'utilisateur avant de poser un tag v<version>\nde deploiement, Claude propose la version SemVer deduite de l'avancement.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Doc - CLAUDE.md : tracabilite Jira dans le code livre + regle commit/tag",
      "type": "docs",
      "modified_files": [
        ".gitignore",
        "CLAUDE.md"
      ],
      "modified_files_details": [
        {
          "path": ".gitignore",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "CLAUDE.md",
          "insertions": 16,
          "deletions": 1,
          "lines_changed": 17
        }
      ],
      "files_changed": 2,
      "insertions": 17,
      "deletions": 2,
      "tags": [
        "v1.2.0"
      ]
    },
    {
      "hash": "d2ead1d1ef2397ac42cdc5dc96e1ea19298e8b28",
      "short_hash": "d2ead1d",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787924099,
      "message": "Evenements - Onglet Physiologie (mesures sante sur la plage de dates) (SPOR-599)\n\n4e onglet de la card de statistiques de la fiche Evenement : les mesures\nsante (health_measures) qui tombent dans la plage de dates de l'evenement\n— aucun rattachement, purement temporel.\n\nDomain\\Health\\EventPhysiologySummary::forWindow() (pur, teste) : fenetre\nelargie de BRACKET_DAYS=7 j de chaque cote (une plage d'evenement fait\nsouvent 1-3 j), delta ancre sur la lecture la plus proche avant le debut\net apres la fin ; a defaut, lectures strictement dans la plage. Variation\ncoloree selon le sens favorable (meme logique que MeasureTrend), min/max,\nnombre de lectures. BloodPressure sur la systolique seule.\n\nEventPhysiologyPresenter (extrait, EventsController deja au plafond Sonar)\n+ HealthMeasureRepository::fetchInRange(). Toujours prive : aucune vue\npublique d'un evenement, aucune fuite de donnees sante.\n\nTests : EventPhysiologySummaryTest (10 cas). Suite complete : 1596 tests.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Evenements - Onglet Physiologie (mesures sante sur la plage de dates) (SPOR-599)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/events.md",
        "src/Domain/Health/EventPhysiologySummary.php",
        "src/Http/Events/EventPhysiologyPresenter.php",
        "src/Http/Events/EventsController.php",
        "src/Http/Health/HealthMeasureRepository.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/event_detail.php",
        "tests/Domain/EventPhysiologySummaryTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 84,
          "deletions": 0,
          "lines_changed": 84
        },
        {
          "path": "docs/events.md",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "src/Domain/Health/EventPhysiologySummary.php",
          "insertions": 145,
          "deletions": 0,
          "lines_changed": 145
        },
        {
          "path": "src/Http/Events/EventPhysiologyPresenter.php",
          "insertions": 89,
          "deletions": 0,
          "lines_changed": 89
        },
        {
          "path": "src/Http/Events/EventsController.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Health/HealthMeasureRepository.php",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "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/event_detail.php",
          "insertions": 71,
          "deletions": 0,
          "lines_changed": 71
        },
        {
          "path": "tests/Domain/EventPhysiologySummaryTest.php",
          "insertions": 166,
          "deletions": 0,
          "lines_changed": 166
        }
      ],
      "files_changed": 11,
      "insertions": 631,
      "deletions": 0
    },
    {
      "hash": "2942effbdb865938ae0a2034c3b7d024d69240ec",
      "short_hash": "2942eff",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787924079,
      "message": "Chrome - Icone d'application (apple-touch-icon) + manifeste PWA (SPOR-598)\n\niOS Safari ignore <link rel=icon> pour l'ecran d'accueil : ajout de\napple-touch-icon (180) + public/site.webmanifest (display standalone,\ntheme #0C0E11, icons 192/512/maskable) dans les DEUX partials de head.\nJeu d'icones aplaties sur fond opaque #0C0E11 (pas d'alpha, sinon bords\nsales sur iOS), committees (pas d'etape de build).\n\npublic/.htaccess : AddType application/manifest+json .webmanifest (certains\nmutualises servent .webmanifest en text/plain -> manifeste ignore).\nPas de service worker (prod FTP-only, contrainte CLAUDE.md) : coquille\nstandalone sans offline.\n\nTests : AppIconTemplateTest (balises dans les 2 chromes, manifeste JSON\nvalide, icones opaques aux bonnes dimensions sur disque).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Chrome - Icone d'application (apple-touch-icon) + manifeste PWA (SPOR-598)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/site.md",
        "public/.htaccess",
        "public/assets/img/logo/apple-touch-icon.png",
        "public/assets/img/logo/icon-192.png",
        "public/assets/img/logo/icon-512.png",
        "public/assets/img/logo/icon-maskable-512.png",
        "public/site.webmanifest",
        "templates/partials/app_head.php",
        "templates/partials/head.php",
        "tests/Infra/View/AppIconTemplateTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        },
        {
          "path": "docs/site.md",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "public/.htaccess",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "public/assets/img/logo/apple-touch-icon.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/logo/icon-192.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/logo/icon-512.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/logo/icon-maskable-512.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/site.webmanifest",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "templates/partials/app_head.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "templates/partials/head.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "tests/Infra/View/AppIconTemplateTest.php",
          "insertions": 104,
          "deletions": 0,
          "lines_changed": 104
        }
      ],
      "files_changed": 12,
      "insertions": 246,
      "deletions": 0
    },
    {
      "hash": "ebb7ea3b4e2f60110cb2d49f36aefbf6d7ab6334",
      "short_hash": "ebb7ea3",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787924062,
      "message": "Trends - Ligne de tendance (regression lineaire) sur les histogrammes (SPOR-597)\n\nDomain\\Training\\LinearTrend::project() (pur, teste) : regression par\nmoindres carres sur les 12 buckets de la serie affichee, valeurs ajustees\nbornees a >= 0, null si < 2 points. Les buckets vides comptent comme 0\n(choix utilisateur : la tendance reflete la regularite reelle).\n\ntemplates/trends.php : cle 'trend' ajoutee a chaque serie weekly/monthly,\nrecalculee a la bascule hebdo/mensuel. trends-charts.js : second dataset\ntype:'line' pointille (--fox-400), au-dessus des barres, exclu du tooltip.\nTableau des totaux mensuels repasse en ordre ante-chronologique.\n\nTests : LinearTrendTest (pente montante/descendante, palier a 0, < 2 points).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Trends - Ligne de tendance (regression lineaire) sur les histogrammes (SPOR-597)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/statistiques.md",
        "public/assets/js/trends-charts.js",
        "src/Domain/Training/LinearTrend.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/trends.php",
        "tests/Domain/LinearTrendTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 39,
          "deletions": 3,
          "lines_changed": 42
        },
        {
          "path": "docs/statistiques.md",
          "insertions": 7,
          "deletions": 1,
          "lines_changed": 8
        },
        {
          "path": "public/assets/js/trends-charts.js",
          "insertions": 37,
          "deletions": 11,
          "lines_changed": 48
        },
        {
          "path": "src/Domain/Training/LinearTrend.php",
          "insertions": 61,
          "deletions": 0,
          "lines_changed": 61
        },
        {
          "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": "templates/trends.php",
          "insertions": 47,
          "deletions": 16,
          "lines_changed": 63
        },
        {
          "path": "tests/Domain/LinearTrendTest.php",
          "insertions": 74,
          "deletions": 0,
          "lines_changed": 74
        }
      ],
      "files_changed": 9,
      "insertions": 268,
      "deletions": 31
    },
    {
      "hash": "77affae0bfe9ce9083223dad4106d4a0e6b64d6d",
      "short_hash": "77affae",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787924042,
      "message": "Auth - Invitations : envoi du lien d'invitation par email (SPOR-596)\n\nAssemblage MIME multipart (Domain\\Mail\\MimeMessage, pur) : sujet encode\nRFC 2047, multipart/alternative (texte + HTML), multipart/related + images\nContent-ID (logo embarque, zero ressource externe qui fuite l'IP). Les 5\nemails transactionnels existants beneficient du sujet encode + Message-ID.\n\nInvitationsController : champ email optionnel a la creation + action\nsendEmail() (POST /admin/invitations/{token}/send) pour (r)envoyer.\nPlafond anti-abus 20 envois / 15 min / admin (table login_attempts).\nMigration 127 : invitations.email_sent_at + email_send_count — l'adresse\ndu destinataire n'est JAMAIS stockee (drapeau + compteur seulement).\n\nMail en local : service mailpit + msmtp dans l'image de dev (exclue du\nFTP ; la prod garde le sendmail de l'hebergeur).\n\nTests : MimeMessage (encodage, multipart, Content-ID), InvitationEmailRequest,\nBrandLogo, InvitationEmailTemplate (rendu HTML + texte, fond sombre, cid:).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Auth - Invitations : envoi du lien d'invitation par email (SPOR-596)",
      "type": "other",
      "modified_files": [
        "Containerfile",
        "References/design/assets/img/email/revontulet-mark.png",
        "References/design/emails/invitation.html",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "compose.yaml",
        "config/config.local.php.dist",
        "config/routes.php",
        "db/migrations/127_add_invitation_email_sent.sql",
        "docs/admin.md",
        "docs/auth.md",
        "public/assets/img/email/revontulet-mark.png",
        "src/Domain/Auth/InvitationEmailRequest.php",
        "src/Domain/Mail/MimeMessage.php",
        "src/Http/Admin/InvitationsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "src/Infra/Mail/BrandLogo.php",
        "src/Infra/Mail/Mailer.php",
        "templates/admin_invitations.php",
        "templates/emails/invitation.php",
        "templates/emails/invitation_text.php",
        "tests/Domain/Auth/InvitationEmailRequestTest.php",
        "tests/Domain/Mail/MimeMessageTest.php",
        "tests/Infra/Mail/BrandLogoTest.php",
        "tests/Infra/View/InvitationEmailTemplateTest.php"
      ],
      "modified_files_details": [
        {
          "path": "Containerfile",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "References/design/assets/img/email/revontulet-mark.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "References/design/emails/invitation.html",
          "insertions": 40,
          "deletions": 25,
          "lines_changed": 65
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 112,
          "deletions": 0,
          "lines_changed": 112
        },
        {
          "path": "compose.yaml",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "config/config.local.php.dist",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "config/routes.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "db/migrations/127_add_invitation_email_sent.sql",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "docs/admin.md",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "docs/auth.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "public/assets/img/email/revontulet-mark.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "src/Domain/Auth/InvitationEmailRequest.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "src/Domain/Mail/MimeMessage.php",
          "insertions": 185,
          "deletions": 0,
          "lines_changed": 185
        },
        {
          "path": "src/Http/Admin/InvitationsController.php",
          "insertions": 144,
          "deletions": 4,
          "lines_changed": 148
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 28,
          "deletions": 1,
          "lines_changed": 29
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 28,
          "deletions": 1,
          "lines_changed": 29
        },
        {
          "path": "src/Infra/Mail/BrandLogo.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "src/Infra/Mail/Mailer.php",
          "insertions": 37,
          "deletions": 11,
          "lines_changed": 48
        },
        {
          "path": "templates/admin_invitations.php",
          "insertions": 36,
          "deletions": 3,
          "lines_changed": 39
        },
        {
          "path": "templates/emails/invitation.php",
          "insertions": 112,
          "deletions": 0,
          "lines_changed": 112
        },
        {
          "path": "templates/emails/invitation_text.php",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "tests/Domain/Auth/InvitationEmailRequestTest.php",
          "insertions": 51,
          "deletions": 0,
          "lines_changed": 51
        },
        {
          "path": "tests/Domain/Mail/MimeMessageTest.php",
          "insertions": 156,
          "deletions": 0,
          "lines_changed": 156
        },
        {
          "path": "tests/Infra/Mail/BrandLogoTest.php",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "tests/Infra/View/InvitationEmailTemplateTest.php",
          "insertions": 89,
          "deletions": 0,
          "lines_changed": 89
        }
      ],
      "files_changed": 26,
      "insertions": 1235,
      "deletions": 49
    },
    {
      "hash": "f96d145b2c711d4ea8f3402a0bc825f1f4b76d99",
      "short_hash": "f96d145",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787906329,
      "message": "CI/CD - Seed : option SEED_SKIP_BLOBS + plafond SeedDump 96 Mo + voie manager OVH pour dump complet (SPOR-595)\n\nLa base reelle est dominee par sessions.tcx_content (traces brutes, ~260 Mo\npour 244 seances). /_ops/seed ne peut pas avaler ca (timeout PHP,\nmax_allowed_packet). Deux voies documentees :\n- SEED_SKIP_BLOBS=1 ./bin/deploy-seed.sh : perl -pe remplace ,0x<hex 400+>\n  par NULL -> dump ~30 Mo via /_ops/seed (stats + carte OK, pas le detail\n  trace/GPX/medias).\n- dump complet -> import SQL server-side du manager OVH.\n\nSeedDump plafond 32 -> 96 Mo. deploy-seed.sh --dump-only ne requiert plus\nURL/OPS_TOKEN.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "CI/CD - Seed : option SEED_SKIP_BLOBS + plafond SeedDump 96 Mo + voie manager OVH pour dump complet ",
      "type": "other",
      "modified_files": [
        "TECHNICAL.md",
        "bin/deploy-seed.sh",
        "src/Infra/Db/SeedDump.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "bin/deploy-seed.sh",
          "insertions": 26,
          "deletions": 13,
          "lines_changed": 39
        },
        {
          "path": "src/Infra/Db/SeedDump.php",
          "insertions": 5,
          "deletions": 3,
          "lines_changed": 8
        }
      ],
      "files_changed": 3,
      "insertions": 45,
      "deletions": 16
    },
    {
      "hash": "0b5ebe6096f4e66050ed0f21c54490c7aae592f2",
      "short_hash": "0b5ebe6",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787904901,
      "message": "CI/CD - Deploy via lftp mirror (SITE CHMOD) : assets statiques en 403 sur OVH sinon (SPOR-595)\n\nSamKirkland/FTP-Deploy-Action n'emet pas de SITE CHMOD -> sur OVH les\nfichiers uploades n'ont pas le droit de lecture \"autre\", Apache 403 sur tous\nles assets statiques (CSS/JS/images servis directement, pas via FCGI). Les\n.php passaient car FCGI tourne en tant que proprietaire.\n\n- assemble dist/ : chmod 755 dossiers / 644 fichiers, 600 config.local.php.\n- deploy : lftp mirror --reverse --delete --parallel=4 (emet SITE CHMOD),\n  exclut var/log/ var/seed/ .well-known/ logs/ .ovhconfig. --delete purge\n  enfin le distant des restes des anciens deploys (README, docs/, bin/...).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "CI/CD - Deploy via lftp mirror (SITE CHMOD) : assets statiques en 403 sur OVH sinon (SPOR-595)",
      "type": "ops",
      "modified_files": [
        ".github/workflows/deploy.yml",
        "TECHNICAL.md"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 30,
          "deletions": 8,
          "lines_changed": 38
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 11,
          "deletions": 3,
          "lines_changed": 14
        }
      ],
      "files_changed": 2,
      "insertions": 41,
      "deletions": 11
    },
    {
      "hash": "81b1b2168cd389eefd1bea052c782e201cbc6a1b",
      "short_hash": "81b1b21",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787904785,
      "message": "CI/CD - Appels /_ops/* avec User-Agent navigateur (anti-bot OVH renvoie 403 sur curl nu) (SPOR-595)\n\nLe vrai blocage des etapes Migrations/Healthcheck du workflow : OVH mutualise\n403 les requetes curl nues (UA \"curl/*\", pas de header Accept). Un navigateur\npasse. deploy.yml + bin/deploy-seed.sh envoient desormais -A \"Mozilla/...\" +\nAccept sur les appels /_ops/*.\n\nTECHNICAL.md / docs/deploiement.md : section \"Specificites OVH mutualise\"\n(DocumentRoot Multisite -> /public, RewriteBase /, anti-bot UA, PHP 8.2).\nCorrige la description erronee \"403 sur tout .php\" de OpsController.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "CI/CD - Appels /_ops/* avec User-Agent navigateur (anti-bot OVH renvoie 403 sur curl nu) (SPOR-595)",
      "type": "other",
      "modified_files": [
        ".github/workflows/deploy.yml",
        "TECHNICAL.md",
        "bin/deploy-seed.sh",
        "docs/deploiement.md"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 11,
          "deletions": 2,
          "lines_changed": 13
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 23,
          "deletions": 9,
          "lines_changed": 32
        },
        {
          "path": "bin/deploy-seed.sh",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "docs/deploiement.md",
          "insertions": 17,
          "deletions": 2,
          "lines_changed": 19
        }
      ],
      "files_changed": 4,
      "insertions": 57,
      "deletions": 15
    },
    {
      "hash": "0d0dde397ab329385b3d0061db281eb878c4087b",
      "short_hash": "0d0dde3",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787904115,
      "message": "CI/CD - Fix rewrite OVH (RewriteBase /) + endpoints ops resistants a la base vide (SPOR-595)\n\nSur OVH mutualise, public/.htaccess sans RewriteBase / : %{REQUEST_FILENAME}\nn'est pas resolu en chemin absolu, le front controller ne route rien\n(/login, /_ops/* en 404). Ajout de RewriteBase / + RewriteCond !-d +\nRewriteRule ^(.*)$ (forme capture) + DirectoryIndex index.php.\n\nLe workflow ne deploie plus le .htaccess RACINE : le DocumentRoot OVH pointe\nsur .../public (multisite), le filet \"docroot trop haut\" reecrirait\n/x -> /public/x -> 404.\n\npublic/{migrate,healthcheck,seed}.php : la lecture anti-bruteforce\n(login_attempts) plantait avant que la table existe -> 500 au tout premier\nmigrate.php d'une instance neuve. try/catch -> throttle \"ouvert\", comme\nOpsController et comme quand la DB est injoignable.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "CI/CD - Fix rewrite OVH (RewriteBase /) + endpoints ops resistants a la base vide (SPOR-595)",
      "type": "fix",
      "modified_files": [
        ".github/workflows/deploy.yml",
        "public/.htaccess",
        "public/healthcheck.php",
        "public/migrate.php",
        "public/seed.php"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "public/.htaccess",
          "insertions": 13,
          "deletions": 4,
          "lines_changed": 17
        },
        {
          "path": "public/healthcheck.php",
          "insertions": 11,
          "deletions": 5,
          "lines_changed": 16
        },
        {
          "path": "public/migrate.php",
          "insertions": 12,
          "deletions": 5,
          "lines_changed": 17
        },
        {
          "path": "public/seed.php",
          "insertions": 10,
          "deletions": 5,
          "lines_changed": 15
        }
      ],
      "files_changed": 5,
      "insertions": 50,
      "deletions": 20
    },
    {
      "hash": "50dcfc89049b07f43fa1d0325d6b0c3af4ebb0c7",
      "short_hash": "50dcfc8",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787874001,
      "message": "CI/CD - Endpoints ops via Slim (/_ops/migrate|healthcheck|seed) : l'hebergeur 403 tout .php hors front controller (SPOR-595)\n\nSur la prod, Apache renvoie 403 sur public/{migrate,healthcheck,seed}.php\n(direct .php interdit hors index.php) : migrate.php n'a jamais pu tourner,\nd'ou la Slim Application Error en homepage (tables absentes).\n\n- Http\\Ops\\OpsController : memes operations exposees en routes Slim, qui\n  passent par index.php. Meme protection (ops_token hash_equals + throttle\n  partage ops:<ip> ; throttle \"ouvert\" si login_attempts absente, cas du\n  1er deploiement). Reutilise Migrator/AdminSeeder/SeedDump/HealthStatus.\n- config/routes.php : GET /_ops/migrate|healthcheck|seed.\n- deploy.yml + bin/deploy-seed.sh appellent /_ops/* au lieu des .php.\n- public/{migrate,healthcheck,seed}.php conserves (local, hebergeurs permissifs).\n\nTeste sur l'app locale : 403 sans token, healthcheck ok, migrate 200, seed\nsans confirm 400. Suite verte (1555).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "CI/CD - Endpoints ops via Slim (/_ops/migrate|healthcheck|seed) : l'hebergeur 403 tout .php hors fro",
      "type": "other",
      "modified_files": [
        ".github/workflows/deploy.yml",
        "TECHNICAL.md",
        "bin/deploy-seed.sh",
        "config/routes.php",
        "docs/deploiement.md",
        "src/Http/Ops/OpsController.php"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 7,
          "deletions": 4,
          "lines_changed": 11
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 26,
          "deletions": 2,
          "lines_changed": 28
        },
        {
          "path": "bin/deploy-seed.sh",
          "insertions": 4,
          "deletions": 4,
          "lines_changed": 8
        },
        {
          "path": "config/routes.php",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "docs/deploiement.md",
          "insertions": 4,
          "deletions": 4,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Ops/OpsController.php",
          "insertions": 245,
          "deletions": 0,
          "lines_changed": 245
        }
      ],
      "files_changed": 6,
      "insertions": 297,
      "deletions": 14
    },
    {
      "hash": "b7786d7d35ebae3244edfaad9d2e5976ec938e5e",
      "short_hash": "b7786d7",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787872079,
      "message": "CI/CD - Deploiement : seul le code applicatif part sur le FTP (SPOR-595)\n\nLe workflow assemblait tout le depot sur le FTP (README, CLAUDE.md,\nTECHNICAL.md, docs/, References/, bin/, tests/, composer.*...), servis en\nclair sur revontulet.fr/... Nouvelle etape \"Assembler dist/\" : cp cible du\nstrict runtime (.htaccess, public/, src/, templates/, config/{routes,\nsettings,config.local}.php, db/migrations/, vendor/, var/{log,cache}/.gitkeep,\nVERSION), tout *.md supprime, puis FTP avec local-dir: ./dist/.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "CI/CD - Deploiement : seul le code applicatif part sur le FTP (SPOR-595)",
      "type": "other",
      "modified_files": [
        ".github/workflows/deploy.yml",
        "TECHNICAL.md",
        "docs/deploiement.md"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 23,
          "deletions": 18,
          "lines_changed": 41
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 15,
          "deletions": 2,
          "lines_changed": 17
        },
        {
          "path": "docs/deploiement.md",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        }
      ],
      "files_changed": 3,
      "insertions": 41,
      "deletions": 22
    },
    {
      "hash": "fa3918c9f27b6f1855872c338cfd0f0f5e79fedf",
      "short_hash": "fa3918c",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787871085,
      "message": "Snapshot WIP : travail cumule non commite (2FA, Evenements, Timeline, i18n, divers)\n\nRegroupement en un seul commit a la demande de l'utilisateur : le working\ntree contenait ~290 fichiers non commites melant plusieurs epics dont les\nfichiers partages (config/routes.php, TECHNICAL.md, mindmap, partials,\ncomposer.json) ne sont pas separables proprement sans hunk-splitting.\n\nContenu principal :\n- 2FA obligatoire TOTP (epic SPOR-584) : enrolement, verification, backup\n  codes, recovery email, step-up, reset admin ; QR encode maison\n  (Infra\\Qr), TotpSecretCipher (sodium), migrations 123-126.\n- Changement d'email confirme par lien (SPOR-589), migration 125.\n- Evenements (epic SPOR-565) : regroupement de seances, galerie medias,\n  selecteur par plage de dates ; migrations 120/122.\n- Timeline globale privee (epic SPOR-527), Http\\Timeline.\n- Internationalisation (Infra\\I18n) : locale utilisateur, textes de quotes\n  et badges localises ; migrations 114-116.\n- Divers : categorie de badge (111), image/duplicate/url/registered/virtual\n  de course (112/115-117), quote ignore (113/530), partenaires de seance\n  (118), autonomie materiel (119), miniature de trace (121), route coverage\n  (SPOR-518, remplace RouteNoveltyController), actions groupees\n  notifications (593), recherche de seances, nombreux tests.\n- Docs utilisateur et TECHNICAL.md correspondants.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Snapshot WIP : travail cumule non commite (2FA, Evenements, Timeline, i18n, divers)",
      "type": "other",
      "modified_files": [
        "AGENTS.md",
        "CLAUDE.md",
        "Containerfile",
        "References/design/app/dashboard.html",
        "References/design/app/sessions.html",
        "References/design/assets/css/app.css",
        "References/design/assets/css/components.css",
        "References/design/assets/js/interactions.js",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "bin/backfill_route_thumbnails.php",
        "composer.json",
        "config/config.local.php.dist",
        "config/routes.php",
        "db/migrations/111_add_badge_category.sql",
        "db/migrations/112_add_race_image.sql",
        "db/migrations/113_add_quote_ignores.sql",
        "db/migrations/114_add_users_locale.sql",
        "db/migrations/115_add_badges_locale_fields.sql",
        "db/migrations/115_add_race_url.sql",
        "db/migrations/116_add_quotes_locale_field.sql",
        "db/migrations/116_add_race_registered.sql",
        "db/migrations/117_add_race_virtual.sql",
        "db/migrations/118_add_session_partners.sql",
        "db/migrations/119_add_gear_autonomy_enabled.sql",
        "db/migrations/120_add_events.sql",
        "db/migrations/121_add_session_route_thumbnail.sql",
        "db/migrations/122_add_event_media.sql",
        "db/migrations/123_add_users_totp.sql",
        "db/migrations/124_add_two_factor_backup_codes.sql",
        "db/migrations/125_add_email_changes.sql",
        "db/migrations/126_add_two_factor_recovery_otps.sql",
        "docs/README.md",
        "docs/admin.md",
        "docs/auth.md",
        "docs/badges.md",
        "docs/dashboard.md",
        "docs/events.md",
        "docs/materiel.md",
        "docs/notifications.md",
        "docs/objectifs.md",
        "docs/profil.md",
        "docs/quotes.md",
        "docs/routes.md",
        "docs/sante.md",
        "docs/sessions.md",
        "docs/site.md",
        "docs/timeline.md",
        "public/assets/css/app.css",
        "public/assets/css/components.css",
        "public/assets/js/dashboard-charts.js",
        "public/assets/js/event-load-chart.js",
        "public/assets/js/event-map.js",
        "public/assets/js/event-media-carousel.js",
        "public/assets/js/event-session-picker.js",
        "public/assets/js/gear-battery-chart.js",
        "public/assets/js/icons.js",
        "public/assets/js/interactions.js",
        "public/assets/js/media-upload.js",
        "public/backfill_route_thumbnails.php",
        "public/index.php",
        "src/Domain/Auth/BackupCodes.php",
        "src/Domain/Auth/EmailChange.php",
        "src/Domain/Auth/StepUpAuth.php",
        "src/Domain/Auth/Totp.php",
        "src/Domain/Auth/TwoFactorRecoveryOtp.php",
        "src/Domain/Content/LocalizedQuoteText.php",
        "src/Domain/Content/NewestQuotes.php",
        "src/Domain/Content/QuoteInput.php",
        "src/Domain/Input/DecimalInput.php",
        "src/Domain/Notifications/NotificationBulkSelection.php",
        "src/Domain/Qr/GaloisField.php",
        "src/Domain/Qr/QrEncoder.php",
        "src/Domain/Qr/QrMaskPenalty.php",
        "src/Domain/Qr/QrMatrix.php",
        "src/Domain/Qr/QrVersion.php",
        "src/Domain/Qr/ReedSolomonEncoder.php",
        "src/Domain/Training/BadgeInput.php",
        "src/Domain/Training/BadgesTimeline.php",
        "src/Domain/Training/CumulativeBadges.php",
        "src/Domain/Training/DayOverDayDelta.php",
        "src/Domain/Training/EventInput.php",
        "src/Domain/Training/GearAutonomy.php",
        "src/Domain/Training/GearBattery.php",
        "src/Domain/Training/GearInput.php",
        "src/Domain/Training/GearPickerOrder.php",
        "src/Domain/Training/OptionalUrl.php",
        "src/Domain/Training/Pagination.php",
        "src/Domain/Training/RaceReactivation.php",
        "src/Domain/Training/RaceRegistrationStatus.php",
        "src/Domain/Training/RecentlyEarnedBadges.php",
        "src/Domain/Training/RouteArcGraph.php",
        "src/Domain/Training/RouteSegmentNovelty.php",
        "src/Domain/Training/RouteThumbnail.php",
        "src/Domain/Training/SessionInput.php",
        "src/Domain/Training/SessionPartnersInput.php",
        "src/Domain/Training/SessionSearchQuery.php",
        "src/Domain/Training/TagCloud.php",
        "src/Domain/Training/Timeline.php",
        "src/Domain/Training/TimelineScale.php",
        "src/Http/Admin/AdminBadgesController.php",
        "src/Http/Admin/AdminChecklistController.php",
        "src/Http/Admin/AdminQuotesController.php",
        "src/Http/Admin/AdminSessionTagsController.php",
        "src/Http/Admin/AdminSportsController.php",
        "src/Http/Admin/AdminUsersController.php",
        "src/Http/Admin/InvitationsController.php",
        "src/Http/Admin/LogsController.php",
        "src/Http/Auth/AuthGuard.php",
        "src/Http/Auth/LoginController.php",
        "src/Http/Auth/PasswordResetController.php",
        "src/Http/Auth/RegisterController.php",
        "src/Http/Auth/StepUpController.php",
        "src/Http/Auth/TwoFactorGate.php",
        "src/Http/Auth/TwoFactorRecoveryController.php",
        "src/Http/Auth/TwoFactorSetupController.php",
        "src/Http/Auth/TwoFactorVerifyController.php",
        "src/Http/Calendar/CalendarController.php",
        "src/Http/Content/QuotesController.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Events/EventBadgeStore.php",
        "src/Http/Events/EventMediaController.php",
        "src/Http/Events/EventMediaStore.php",
        "src/Http/Events/EventSessionPicker.php",
        "src/Http/Events/EventsController.php",
        "src/Http/Events/EventsSidebarCounter.php",
        "src/Http/Gear/GearAutoAssignController.php",
        "src/Http/Gear/GearBatteryStore.php",
        "src/Http/Gear/GearController.php",
        "src/Http/Gear/GearMediaController.php",
        "src/Http/Gear/GearOptionWear.php",
        "src/Http/Gear/GearPickerOrderMap.php",
        "src/Http/Goals/GoalFormHelpers.php",
        "src/Http/Goals/GoalsController.php",
        "src/Http/Goals/RacesController.php",
        "src/Http/Goals/YearlyController.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/PainInjuriesController.php",
        "src/Http/Media/MediaUploadBatch.php",
        "src/Http/Notifications/NotificationsController.php",
        "src/Http/Profile/ProfileController.php",
        "src/Http/Records/BadgeCatalog.php",
        "src/Http/Records/BadgesController.php",
        "src/Http/Records/RecordsController.php",
        "src/Http/Routes/RoutePlannerController.php",
        "src/Http/Routes/RoutesController.php",
        "src/Http/Sessions/RouteCoverageController.php",
        "src/Http/Sessions/RouteNoveltyController.php",
        "src/Http/Sessions/RouteReferencePolylines.php",
        "src/Http/Sessions/SessionCardBadges.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionGearStore.php",
        "src/Http/Sessions/SessionMediaController.php",
        "src/Http/Sessions/SessionMediaStore.php",
        "src/Http/Sessions/SessionPartnersStore.php",
        "src/Http/Sessions/SessionPresenter.php",
        "src/Http/Sessions/SessionSearchSql.php",
        "src/Http/Sessions/SessionTagsStore.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Http/Settings/AgeCategoryPresenter.php",
        "src/Http/Settings/EmailChangeController.php",
        "src/Http/Settings/SettingsController.php",
        "src/Http/Settings/TwoFactorSettingsController.php",
        "src/Http/Timeline/TimelineController.php",
        "src/Http/Trends/TrendsController.php",
        "src/Infra/Db/RouteThumbnailBackfill.php",
        "src/Infra/I18n/DateLabels.php",
        "src/Infra/I18n/Locale.php",
        "src/Infra/I18n/Translator.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "src/Infra/Qr/QrPngRenderer.php",
        "src/Infra/Security/TotpSecretCipher.php",
        "src/Infra/Security/TwoFactorVerifier.php",
        "src/Infra/View/ViewRenderer.php",
        "templates/2fa_setup.php",
        "templates/2fa_stepup.php",
        "templates/2fa_verify.php",
        "templates/about.php",
        "templates/admin_badges.php",
        "templates/admin_checklist.php",
        "templates/admin_invitations.php",
        "templates/admin_logs.php",
        "templates/admin_quotes.php",
        "templates/admin_sports.php",
        "templates/admin_tags.php",
        "templates/admin_users.php",
        "templates/badges.php",
        "templates/blog.php",
        "templates/blog_post.php",
        "templates/calendar.php",
        "templates/dashboard.php",
        "templates/donate.php",
        "templates/email_confirm_result.php",
        "templates/emails/email_change_confirm.php",
        "templates/emails/email_changed_notice.php",
        "templates/emails/password_changed.php",
        "templates/emails/password_reset.php",
        "templates/emails/two_factor_recovery_otp.php",
        "templates/event_detail.php",
        "templates/events.php",
        "templates/forgot_password.php",
        "templates/gear.php",
        "templates/gear_auto_assign_review.php",
        "templates/gear_detail.php",
        "templates/goals.php",
        "templates/health.php",
        "templates/home.php",
        "templates/login.php",
        "templates/notifications.php",
        "templates/onboarding.php",
        "templates/partials/admin_tabs.php",
        "templates/partials/app_head.php",
        "templates/partials/app_sidebar.php",
        "templates/partials/app_topbar.php",
        "templates/partials/badge_edit_modal.php",
        "templates/partials/badge_form_fields.php",
        "templates/partials/checklist_catalog_chip.php",
        "templates/partials/checklist_edit_modal.php",
        "templates/partials/checklist_form_fields.php",
        "templates/partials/event_session_picker.php",
        "templates/partials/foot.php",
        "templates/partials/gear_battery_modal.php",
        "templates/partials/gear_edit_modal.php",
        "templates/partials/gear_form_fields.php",
        "templates/partials/gear_service_log_modal.php",
        "templates/partials/goal_edit_modal.php",
        "templates/partials/goal_form_fields.php",
        "templates/partials/head.php",
        "templates/partials/media_upload_modal.php",
        "templates/partials/medical_document_card.php",
        "templates/partials/notification_row.php",
        "templates/partials/quote_edit_modal.php",
        "templates/partials/quote_form_fields.php",
        "templates/partials/quote_private_edit_modal.php",
        "templates/partials/quote_public_card.php",
        "templates/partials/race_duplicate_modal.php",
        "templates/partials/race_link_modal.php",
        "templates/partials/race_link_status.php",
        "templates/partials/session_modals.php",
        "templates/partials/sport_delete_modals.php",
        "templates/partials/sport_edit_modal.php",
        "templates/partials/sport_form_fields.php",
        "templates/partials/subsport_delete_modals.php",
        "templates/partials/subsport_edit_modal.php",
        "templates/partials/subsport_form_fields.php",
        "templates/partials/tag_form_fields.php",
        "templates/partials/yearly_duplicate_modal.php",
        "templates/quotes.php",
        "templates/races.php",
        "templates/records.php",
        "templates/register.php",
        "templates/reset_password.php",
        "templates/roadmap.php",
        "templates/route_coverage.php",
        "templates/session_detail.php",
        "templates/sessions.php",
        "templates/settings.php",
        "templates/settings_2fa.php",
        "templates/settings_email.php",
        "templates/timeline.php",
        "templates/trends.php",
        "templates/yearly.php",
        "tests/Domain/BackupCodesTest.php",
        "tests/Domain/BadgeInputTest.php",
        "tests/Domain/BadgesTimelineTest.php",
        "tests/Domain/Content/LocalizedQuoteTextTest.php",
        "tests/Domain/CumulativeBadgesTest.php",
        "tests/Domain/DayOverDayDeltaTest.php",
        "tests/Domain/DecimalInputTest.php",
        "tests/Domain/EmailChangeTest.php",
        "tests/Domain/EventInputTest.php",
        "tests/Domain/GaloisFieldTest.php",
        "tests/Domain/GearAutonomyTest.php",
        "tests/Domain/GearBatteryTest.php",
        "tests/Domain/GearInputTest.php",
        "tests/Domain/GearPickerOrderTest.php",
        "tests/Domain/NewestQuotesTest.php",
        "tests/Domain/Notifications/NotificationBulkSelectionTest.php",
        "tests/Domain/OptionalUrlTest.php",
        "tests/Domain/PaginationTest.php",
        "tests/Domain/QrEncoderTest.php",
        "tests/Domain/QrMaskPenaltyTest.php",
        "tests/Domain/QuoteInputTest.php",
        "tests/Domain/RaceReactivationTest.php",
        "tests/Domain/RaceRegistrationStatusTest.php",
        "tests/Domain/RecentlyEarnedBadgesTest.php",
        "tests/Domain/ReedSolomonEncoderTest.php",
        "tests/Domain/RouteSegmentNoveltyTest.php",
        "tests/Domain/RouteThumbnailTest.php",
        "tests/Domain/SessionPartnersInputTest.php",
        "tests/Domain/SessionSearchQueryTest.php",
        "tests/Domain/StepUpAuthTest.php",
        "tests/Domain/TagCloudTest.php",
        "tests/Domain/TimelineTest.php",
        "tests/Domain/TotpTest.php",
        "tests/Domain/TwoFactorRecoveryOtpTest.php",
        "tests/Http/Records/BadgeCatalogLocalizationTest.php",
        "tests/Http/Records/BadgeCatalogTest.php",
        "tests/Http/Sessions/SessionPresenterTest.php",
        "tests/Http/Sessions/SessionSearchSqlTest.php",
        "tests/Infra/I18n/DateLabelsTest.php",
        "tests/Infra/I18n/LocaleTest.php",
        "tests/Infra/I18n/NotificationsCatalogTest.php",
        "tests/Infra/I18n/TranslatorTest.php",
        "tests/Infra/Security/TotpSecretCipherTest.php"
      ],
      "modified_files_details": [
        {
          "path": "AGENTS.md",
          "insertions": 2,
          "deletions": 4,
          "lines_changed": 6
        },
        {
          "path": "CLAUDE.md",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "Containerfile",
          "insertions": 6,
          "deletions": 3,
          "lines_changed": 9
        },
        {
          "path": "References/design/app/dashboard.html",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "References/design/app/sessions.html",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "References/design/assets/css/components.css",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "References/design/assets/js/interactions.js",
          "insertions": 110,
          "deletions": 0,
          "lines_changed": 110
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 123,
          "deletions": 0,
          "lines_changed": 123
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 6486,
          "deletions": 2,
          "lines_changed": 6488
        },
        {
          "path": "bin/backfill_route_thumbnails.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "composer.json",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "config/config.local.php.dist",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "config/routes.php",
          "insertions": 92,
          "deletions": 4,
          "lines_changed": 96
        },
        {
          "path": "db/migrations/111_add_badge_category.sql",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "db/migrations/112_add_race_image.sql",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "db/migrations/113_add_quote_ignores.sql",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "db/migrations/114_add_users_locale.sql",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "db/migrations/115_add_badges_locale_fields.sql",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "db/migrations/115_add_race_url.sql",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "db/migrations/116_add_quotes_locale_field.sql",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "db/migrations/116_add_race_registered.sql",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "db/migrations/117_add_race_virtual.sql",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "db/migrations/118_add_session_partners.sql",
          "insertions": 29,
          "deletions": 0,
          "lines_changed": 29
        },
        {
          "path": "db/migrations/119_add_gear_autonomy_enabled.sql",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "db/migrations/120_add_events.sql",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "db/migrations/121_add_session_route_thumbnail.sql",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "db/migrations/122_add_event_media.sql",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "db/migrations/123_add_users_totp.sql",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "db/migrations/124_add_two_factor_backup_codes.sql",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "db/migrations/125_add_email_changes.sql",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "db/migrations/126_add_two_factor_recovery_otps.sql",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "docs/README.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "docs/admin.md",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "docs/auth.md",
          "insertions": 58,
          "deletions": 1,
          "lines_changed": 59
        },
        {
          "path": "docs/badges.md",
          "insertions": 34,
          "deletions": 5,
          "lines_changed": 39
        },
        {
          "path": "docs/dashboard.md",
          "insertions": 10,
          "deletions": 3,
          "lines_changed": 13
        },
        {
          "path": "docs/events.md",
          "insertions": 131,
          "deletions": 0,
          "lines_changed": 131
        },
        {
          "path": "docs/materiel.md",
          "insertions": 118,
          "deletions": 14,
          "lines_changed": 132
        },
        {
          "path": "docs/notifications.md",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 94,
          "deletions": 8,
          "lines_changed": 102
        },
        {
          "path": "docs/profil.md",
          "insertions": 54,
          "deletions": 3,
          "lines_changed": 57
        },
        {
          "path": "docs/quotes.md",
          "insertions": 16,
          "deletions": 3,
          "lines_changed": 19
        },
        {
          "path": "docs/routes.md",
          "insertions": 29,
          "deletions": 13,
          "lines_changed": 42
        },
        {
          "path": "docs/sante.md",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "docs/sessions.md",
          "insertions": 49,
          "deletions": 6,
          "lines_changed": 55
        },
        {
          "path": "docs/site.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "docs/timeline.md",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 90,
          "deletions": 1,
          "lines_changed": 91
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 228,
          "deletions": 0,
          "lines_changed": 228
        },
        {
          "path": "public/assets/js/dashboard-charts.js",
          "insertions": 104,
          "deletions": 0,
          "lines_changed": 104
        },
        {
          "path": "public/assets/js/event-load-chart.js",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "public/assets/js/event-map.js",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "public/assets/js/event-media-carousel.js",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "public/assets/js/event-session-picker.js",
          "insertions": 51,
          "deletions": 0,
          "lines_changed": 51
        },
        {
          "path": "public/assets/js/gear-battery-chart.js",
          "insertions": 63,
          "deletions": 30,
          "lines_changed": 93
        },
        {
          "path": "public/assets/js/icons.js",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 643,
          "deletions": 3,
          "lines_changed": 646
        },
        {
          "path": "public/assets/js/media-upload.js",
          "insertions": 130,
          "deletions": 0,
          "lines_changed": 130
        },
        {
          "path": "public/backfill_route_thumbnails.php",
          "insertions": 96,
          "deletions": 0,
          "lines_changed": 96
        },
        {
          "path": "public/index.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "src/Domain/Auth/BackupCodes.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "src/Domain/Auth/EmailChange.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "src/Domain/Auth/StepUpAuth.php",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "src/Domain/Auth/Totp.php",
          "insertions": 117,
          "deletions": 0,
          "lines_changed": 117
        },
        {
          "path": "src/Domain/Auth/TwoFactorRecoveryOtp.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "src/Domain/Content/LocalizedQuoteText.php",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "src/Domain/Content/NewestQuotes.php",
          "insertions": 42,
          "deletions": 0,
          "lines_changed": 42
        },
        {
          "path": "src/Domain/Content/QuoteInput.php",
          "insertions": 7,
          "deletions": 9,
          "lines_changed": 16
        },
        {
          "path": "src/Domain/Input/DecimalInput.php",
          "insertions": 55,
          "deletions": 0,
          "lines_changed": 55
        },
        {
          "path": "src/Domain/Notifications/NotificationBulkSelection.php",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "src/Domain/Qr/GaloisField.php",
          "insertions": 59,
          "deletions": 0,
          "lines_changed": 59
        },
        {
          "path": "src/Domain/Qr/QrEncoder.php",
          "insertions": 344,
          "deletions": 0,
          "lines_changed": 344
        },
        {
          "path": "src/Domain/Qr/QrMaskPenalty.php",
          "insertions": 159,
          "deletions": 0,
          "lines_changed": 159
        },
        {
          "path": "src/Domain/Qr/QrMatrix.php",
          "insertions": 56,
          "deletions": 0,
          "lines_changed": 56
        },
        {
          "path": "src/Domain/Qr/QrVersion.php",
          "insertions": 102,
          "deletions": 0,
          "lines_changed": 102
        },
        {
          "path": "src/Domain/Qr/ReedSolomonEncoder.php",
          "insertions": 52,
          "deletions": 0,
          "lines_changed": 52
        },
        {
          "path": "src/Domain/Training/BadgeInput.php",
          "insertions": 66,
          "deletions": 2,
          "lines_changed": 68
        },
        {
          "path": "src/Domain/Training/BadgesTimeline.php",
          "insertions": 118,
          "deletions": 26,
          "lines_changed": 144
        },
        {
          "path": "src/Domain/Training/CumulativeBadges.php",
          "insertions": 41,
          "deletions": 9,
          "lines_changed": 50
        },
        {
          "path": "src/Domain/Training/DayOverDayDelta.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "src/Domain/Training/EventInput.php",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "src/Domain/Training/GearAutonomy.php",
          "insertions": 171,
          "deletions": 0,
          "lines_changed": 171
        },
        {
          "path": "src/Domain/Training/GearBattery.php",
          "insertions": 62,
          "deletions": 23,
          "lines_changed": 85
        },
        {
          "path": "src/Domain/Training/GearInput.php",
          "insertions": 19,
          "deletions": 1,
          "lines_changed": 20
        },
        {
          "path": "src/Domain/Training/GearPickerOrder.php",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "src/Domain/Training/OptionalUrl.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "src/Domain/Training/Pagination.php",
          "insertions": 47,
          "deletions": 0,
          "lines_changed": 47
        },
        {
          "path": "src/Domain/Training/RaceReactivation.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "src/Domain/Training/RaceRegistrationStatus.php",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "src/Domain/Training/RecentlyEarnedBadges.php",
          "insertions": 19,
          "deletions": 7,
          "lines_changed": 26
        },
        {
          "path": "src/Domain/Training/RouteArcGraph.php",
          "insertions": 5,
          "deletions": 4,
          "lines_changed": 9
        },
        {
          "path": "src/Domain/Training/RouteSegmentNovelty.php",
          "insertions": 145,
          "deletions": 28,
          "lines_changed": 173
        },
        {
          "path": "src/Domain/Training/RouteThumbnail.php",
          "insertions": 178,
          "deletions": 0,
          "lines_changed": 178
        },
        {
          "path": "src/Domain/Training/SessionInput.php",
          "insertions": 9,
          "deletions": 4,
          "lines_changed": 13
        },
        {
          "path": "src/Domain/Training/SessionPartnersInput.php",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        },
        {
          "path": "src/Domain/Training/SessionSearchQuery.php",
          "insertions": 186,
          "deletions": 0,
          "lines_changed": 186
        },
        {
          "path": "src/Domain/Training/TagCloud.php",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "src/Domain/Training/Timeline.php",
          "insertions": 64,
          "deletions": 0,
          "lines_changed": 64
        },
        {
          "path": "src/Domain/Training/TimelineScale.php",
          "insertions": 67,
          "deletions": 0,
          "lines_changed": 67
        },
        {
          "path": "src/Http/Admin/AdminBadgesController.php",
          "insertions": 33,
          "deletions": 4,
          "lines_changed": 37
        },
        {
          "path": "src/Http/Admin/AdminChecklistController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/AdminQuotesController.php",
          "insertions": 18,
          "deletions": 8,
          "lines_changed": 26
        },
        {
          "path": "src/Http/Admin/AdminSessionTagsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/AdminSportsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/AdminUsersController.php",
          "insertions": 127,
          "deletions": 0,
          "lines_changed": 127
        },
        {
          "path": "src/Http/Admin/InvitationsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/LogsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Auth/AuthGuard.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "src/Http/Auth/LoginController.php",
          "insertions": 13,
          "deletions": 4,
          "lines_changed": 17
        },
        {
          "path": "src/Http/Auth/PasswordResetController.php",
          "insertions": 8,
          "deletions": 2,
          "lines_changed": 10
        },
        {
          "path": "src/Http/Auth/RegisterController.php",
          "insertions": 14,
          "deletions": 4,
          "lines_changed": 18
        },
        {
          "path": "src/Http/Auth/StepUpController.php",
          "insertions": 145,
          "deletions": 0,
          "lines_changed": 145
        },
        {
          "path": "src/Http/Auth/TwoFactorGate.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "src/Http/Auth/TwoFactorRecoveryController.php",
          "insertions": 190,
          "deletions": 0,
          "lines_changed": 190
        },
        {
          "path": "src/Http/Auth/TwoFactorSetupController.php",
          "insertions": 203,
          "deletions": 0,
          "lines_changed": 203
        },
        {
          "path": "src/Http/Auth/TwoFactorVerifyController.php",
          "insertions": 106,
          "deletions": 0,
          "lines_changed": 106
        },
        {
          "path": "src/Http/Calendar/CalendarController.php",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "src/Http/Content/QuotesController.php",
          "insertions": 132,
          "deletions": 11,
          "lines_changed": 143
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 174,
          "deletions": 20,
          "lines_changed": 194
        },
        {
          "path": "src/Http/Events/EventBadgeStore.php",
          "insertions": 49,
          "deletions": 0,
          "lines_changed": 49
        },
        {
          "path": "src/Http/Events/EventMediaController.php",
          "insertions": 129,
          "deletions": 0,
          "lines_changed": 129
        },
        {
          "path": "src/Http/Events/EventMediaStore.php",
          "insertions": 95,
          "deletions": 0,
          "lines_changed": 95
        },
        {
          "path": "src/Http/Events/EventSessionPicker.php",
          "insertions": 150,
          "deletions": 0,
          "lines_changed": 150
        },
        {
          "path": "src/Http/Events/EventsController.php",
          "insertions": 687,
          "deletions": 0,
          "lines_changed": 687
        },
        {
          "path": "src/Http/Events/EventsSidebarCounter.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "src/Http/Gear/GearAutoAssignController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Gear/GearBatteryStore.php",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 150,
          "deletions": 67,
          "lines_changed": 217
        },
        {
          "path": "src/Http/Gear/GearMediaController.php",
          "insertions": 20,
          "deletions": 65,
          "lines_changed": 85
        },
        {
          "path": "src/Http/Gear/GearOptionWear.php",
          "insertions": 88,
          "deletions": 0,
          "lines_changed": 88
        },
        {
          "path": "src/Http/Gear/GearPickerOrderMap.php",
          "insertions": 121,
          "deletions": 0,
          "lines_changed": 121
        },
        {
          "path": "src/Http/Goals/GoalFormHelpers.php",
          "insertions": 67,
          "deletions": 4,
          "lines_changed": 71
        },
        {
          "path": "src/Http/Goals/GoalsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 359,
          "deletions": 36,
          "lines_changed": 395
        },
        {
          "path": "src/Http/Goals/YearlyController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 18,
          "deletions": 8,
          "lines_changed": 26
        },
        {
          "path": "src/Http/Health/PainInjuriesController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Media/MediaUploadBatch.php",
          "insertions": 168,
          "deletions": 0,
          "lines_changed": 168
        },
        {
          "path": "src/Http/Notifications/NotificationsController.php",
          "insertions": 66,
          "deletions": 0,
          "lines_changed": 66
        },
        {
          "path": "src/Http/Profile/ProfileController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Records/BadgeCatalog.php",
          "insertions": 184,
          "deletions": 3,
          "lines_changed": 187
        },
        {
          "path": "src/Http/Records/BadgesController.php",
          "insertions": 75,
          "deletions": 21,
          "lines_changed": 96
        },
        {
          "path": "src/Http/Records/RecordsController.php",
          "insertions": 5,
          "deletions": 2,
          "lines_changed": 7
        },
        {
          "path": "src/Http/Routes/RoutePlannerController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Routes/RoutesController.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Http/Sessions/RouteCoverageController.php",
          "insertions": 186,
          "deletions": 0,
          "lines_changed": 186
        },
        {
          "path": "src/Http/Sessions/RouteNoveltyController.php",
          "insertions": 0,
          "deletions": 127,
          "lines_changed": 127
        },
        {
          "path": "src/Http/Sessions/RouteReferencePolylines.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "src/Http/Sessions/SessionCardBadges.php",
          "insertions": 81,
          "deletions": 0,
          "lines_changed": 81
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 204,
          "deletions": 56,
          "lines_changed": 260
        },
        {
          "path": "src/Http/Sessions/SessionGearStore.php",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "src/Http/Sessions/SessionMediaController.php",
          "insertions": 23,
          "deletions": 66,
          "lines_changed": 89
        },
        {
          "path": "src/Http/Sessions/SessionMediaStore.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "src/Http/Sessions/SessionPartnersStore.php",
          "insertions": 102,
          "deletions": 0,
          "lines_changed": 102
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 178,
          "deletions": 37,
          "lines_changed": 215
        },
        {
          "path": "src/Http/Sessions/SessionSearchSql.php",
          "insertions": 57,
          "deletions": 0,
          "lines_changed": 57
        },
        {
          "path": "src/Http/Sessions/SessionTagsStore.php",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 292,
          "deletions": 113,
          "lines_changed": 405
        },
        {
          "path": "src/Http/Settings/AgeCategoryPresenter.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Settings/EmailChangeController.php",
          "insertions": 209,
          "deletions": 0,
          "lines_changed": 209
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 45,
          "deletions": 7,
          "lines_changed": 52
        },
        {
          "path": "src/Http/Settings/TwoFactorSettingsController.php",
          "insertions": 180,
          "deletions": 0,
          "lines_changed": 180
        },
        {
          "path": "src/Http/Timeline/TimelineController.php",
          "insertions": 349,
          "deletions": 0,
          "lines_changed": 349
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 21,
          "deletions": 2,
          "lines_changed": 23
        },
        {
          "path": "src/Infra/Db/RouteThumbnailBackfill.php",
          "insertions": 73,
          "deletions": 0,
          "lines_changed": 73
        },
        {
          "path": "src/Infra/I18n/DateLabels.php",
          "insertions": 100,
          "deletions": 0,
          "lines_changed": 100
        },
        {
          "path": "src/Infra/I18n/Locale.php",
          "insertions": 121,
          "deletions": 0,
          "lines_changed": 121
        },
        {
          "path": "src/Infra/I18n/Translator.php",
          "insertions": 91,
          "deletions": 0,
          "lines_changed": 91
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 1660,
          "deletions": 0,
          "lines_changed": 1660
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 1652,
          "deletions": 0,
          "lines_changed": 1652
        },
        {
          "path": "src/Infra/Qr/QrPngRenderer.php",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "src/Infra/Security/TotpSecretCipher.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "src/Infra/Security/TwoFactorVerifier.php",
          "insertions": 89,
          "deletions": 0,
          "lines_changed": 89
        },
        {
          "path": "src/Infra/View/ViewRenderer.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "templates/2fa_setup.php",
          "insertions": 106,
          "deletions": 0,
          "lines_changed": 106
        },
        {
          "path": "templates/2fa_stepup.php",
          "insertions": 126,
          "deletions": 0,
          "lines_changed": 126
        },
        {
          "path": "templates/2fa_verify.php",
          "insertions": 204,
          "deletions": 0,
          "lines_changed": 204
        },
        {
          "path": "templates/about.php",
          "insertions": 27,
          "deletions": 36,
          "lines_changed": 63
        },
        {
          "path": "templates/admin_badges.php",
          "insertions": 76,
          "deletions": 17,
          "lines_changed": 93
        },
        {
          "path": "templates/admin_checklist.php",
          "insertions": 15,
          "deletions": 14,
          "lines_changed": 29
        },
        {
          "path": "templates/admin_invitations.php",
          "insertions": 27,
          "deletions": 19,
          "lines_changed": 46
        },
        {
          "path": "templates/admin_logs.php",
          "insertions": 26,
          "deletions": 18,
          "lines_changed": 44
        },
        {
          "path": "templates/admin_quotes.php",
          "insertions": 16,
          "deletions": 15,
          "lines_changed": 31
        },
        {
          "path": "templates/admin_sports.php",
          "insertions": 28,
          "deletions": 27,
          "lines_changed": 55
        },
        {
          "path": "templates/admin_tags.php",
          "insertions": 16,
          "deletions": 15,
          "lines_changed": 31
        },
        {
          "path": "templates/admin_users.php",
          "insertions": 92,
          "deletions": 0,
          "lines_changed": 92
        },
        {
          "path": "templates/badges.php",
          "insertions": 317,
          "deletions": 38,
          "lines_changed": 355
        },
        {
          "path": "templates/blog.php",
          "insertions": 27,
          "deletions": 31,
          "lines_changed": 58
        },
        {
          "path": "templates/blog_post.php",
          "insertions": 21,
          "deletions": 36,
          "lines_changed": 57
        },
        {
          "path": "templates/calendar.php",
          "insertions": 27,
          "deletions": 18,
          "lines_changed": 45
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 178,
          "deletions": 97,
          "lines_changed": 275
        },
        {
          "path": "templates/donate.php",
          "insertions": 30,
          "deletions": 30,
          "lines_changed": 60
        },
        {
          "path": "templates/email_confirm_result.php",
          "insertions": 51,
          "deletions": 0,
          "lines_changed": 51
        },
        {
          "path": "templates/emails/email_change_confirm.php",
          "insertions": 82,
          "deletions": 0,
          "lines_changed": 82
        },
        {
          "path": "templates/emails/email_changed_notice.php",
          "insertions": 71,
          "deletions": 0,
          "lines_changed": 71
        },
        {
          "path": "templates/emails/password_changed.php",
          "insertions": 12,
          "deletions": 11,
          "lines_changed": 23
        },
        {
          "path": "templates/emails/password_reset.php",
          "insertions": 13,
          "deletions": 14,
          "lines_changed": 27
        },
        {
          "path": "templates/emails/two_factor_recovery_otp.php",
          "insertions": 84,
          "deletions": 0,
          "lines_changed": 84
        },
        {
          "path": "templates/event_detail.php",
          "insertions": 557,
          "deletions": 0,
          "lines_changed": 557
        },
        {
          "path": "templates/events.php",
          "insertions": 235,
          "deletions": 0,
          "lines_changed": 235
        },
        {
          "path": "templates/forgot_password.php",
          "insertions": 15,
          "deletions": 14,
          "lines_changed": 29
        },
        {
          "path": "templates/gear.php",
          "insertions": 86,
          "deletions": 41,
          "lines_changed": 127
        },
        {
          "path": "templates/gear_auto_assign_review.php",
          "insertions": 15,
          "deletions": 14,
          "lines_changed": 29
        },
        {
          "path": "templates/gear_detail.php",
          "insertions": 141,
          "deletions": 89,
          "lines_changed": 230
        },
        {
          "path": "templates/goals.php",
          "insertions": 28,
          "deletions": 32,
          "lines_changed": 60
        },
        {
          "path": "templates/health.php",
          "insertions": 4,
          "deletions": 4,
          "lines_changed": 8
        },
        {
          "path": "templates/home.php",
          "insertions": 29,
          "deletions": 26,
          "lines_changed": 55
        },
        {
          "path": "templates/login.php",
          "insertions": 31,
          "deletions": 28,
          "lines_changed": 59
        },
        {
          "path": "templates/notifications.php",
          "insertions": 31,
          "deletions": 10,
          "lines_changed": 41
        },
        {
          "path": "templates/onboarding.php",
          "insertions": 33,
          "deletions": 27,
          "lines_changed": 60
        },
        {
          "path": "templates/partials/admin_tabs.php",
          "insertions": 12,
          "deletions": 8,
          "lines_changed": 20
        },
        {
          "path": "templates/partials/app_head.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 42,
          "deletions": 24,
          "lines_changed": 66
        },
        {
          "path": "templates/partials/app_topbar.php",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "templates/partials/badge_edit_modal.php",
          "insertions": 11,
          "deletions": 10,
          "lines_changed": 21
        },
        {
          "path": "templates/partials/badge_form_fields.php",
          "insertions": 75,
          "deletions": 33,
          "lines_changed": 108
        },
        {
          "path": "templates/partials/checklist_catalog_chip.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "templates/partials/checklist_edit_modal.php",
          "insertions": 5,
          "deletions": 4,
          "lines_changed": 9
        },
        {
          "path": "templates/partials/checklist_form_fields.php",
          "insertions": 7,
          "deletions": 6,
          "lines_changed": 13
        },
        {
          "path": "templates/partials/event_session_picker.php",
          "insertions": 72,
          "deletions": 0,
          "lines_changed": 72
        },
        {
          "path": "templates/partials/foot.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "templates/partials/gear_battery_modal.php",
          "insertions": 40,
          "deletions": 24,
          "lines_changed": 64
        },
        {
          "path": "templates/partials/gear_edit_modal.php",
          "insertions": 11,
          "deletions": 10,
          "lines_changed": 21
        },
        {
          "path": "templates/partials/gear_form_fields.php",
          "insertions": 63,
          "deletions": 48,
          "lines_changed": 111
        },
        {
          "path": "templates/partials/gear_service_log_modal.php",
          "insertions": 14,
          "deletions": 13,
          "lines_changed": 27
        },
        {
          "path": "templates/partials/goal_edit_modal.php",
          "insertions": 12,
          "deletions": 8,
          "lines_changed": 20
        },
        {
          "path": "templates/partials/goal_form_fields.php",
          "insertions": 139,
          "deletions": 61,
          "lines_changed": 200
        },
        {
          "path": "templates/partials/head.php",
          "insertions": 9,
          "deletions": 7,
          "lines_changed": 16
        },
        {
          "path": "templates/partials/media_upload_modal.php",
          "insertions": 59,
          "deletions": 0,
          "lines_changed": 59
        },
        {
          "path": "templates/partials/medical_document_card.php",
          "insertions": 16,
          "deletions": 15,
          "lines_changed": 31
        },
        {
          "path": "templates/partials/notification_row.php",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "templates/partials/quote_edit_modal.php",
          "insertions": 11,
          "deletions": 10,
          "lines_changed": 21
        },
        {
          "path": "templates/partials/quote_form_fields.php",
          "insertions": 23,
          "deletions": 6,
          "lines_changed": 29
        },
        {
          "path": "templates/partials/quote_private_edit_modal.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/partials/quote_public_card.php",
          "insertions": 13,
          "deletions": 1,
          "lines_changed": 14
        },
        {
          "path": "templates/partials/race_duplicate_modal.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "templates/partials/race_link_modal.php",
          "insertions": 9,
          "deletions": 8,
          "lines_changed": 17
        },
        {
          "path": "templates/partials/race_link_status.php",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "templates/partials/session_modals.php",
          "insertions": 91,
          "deletions": 66,
          "lines_changed": 157
        },
        {
          "path": "templates/partials/sport_delete_modals.php",
          "insertions": 10,
          "deletions": 9,
          "lines_changed": 19
        },
        {
          "path": "templates/partials/sport_edit_modal.php",
          "insertions": 8,
          "deletions": 7,
          "lines_changed": 15
        },
        {
          "path": "templates/partials/sport_form_fields.php",
          "insertions": 6,
          "deletions": 5,
          "lines_changed": 11
        },
        {
          "path": "templates/partials/subsport_delete_modals.php",
          "insertions": 14,
          "deletions": 11,
          "lines_changed": 25
        },
        {
          "path": "templates/partials/subsport_edit_modal.php",
          "insertions": 8,
          "deletions": 7,
          "lines_changed": 15
        },
        {
          "path": "templates/partials/subsport_form_fields.php",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "templates/partials/tag_form_fields.php",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "templates/partials/yearly_duplicate_modal.php",
          "insertions": 7,
          "deletions": 6,
          "lines_changed": 13
        },
        {
          "path": "templates/quotes.php",
          "insertions": 35,
          "deletions": 7,
          "lines_changed": 42
        },
        {
          "path": "templates/races.php",
          "insertions": 87,
          "deletions": 34,
          "lines_changed": 121
        },
        {
          "path": "templates/records.php",
          "insertions": 53,
          "deletions": 45,
          "lines_changed": 98
        },
        {
          "path": "templates/register.php",
          "insertions": 31,
          "deletions": 21,
          "lines_changed": 52
        },
        {
          "path": "templates/reset_password.php",
          "insertions": 25,
          "deletions": 15,
          "lines_changed": 40
        },
        {
          "path": "templates/roadmap.php",
          "insertions": 32,
          "deletions": 30,
          "lines_changed": 62
        },
        {
          "path": "templates/route_coverage.php",
          "insertions": 188,
          "deletions": 0,
          "lines_changed": 188
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 431,
          "deletions": 327,
          "lines_changed": 758
        },
        {
          "path": "templates/sessions.php",
          "insertions": 121,
          "deletions": 58,
          "lines_changed": 179
        },
        {
          "path": "templates/settings.php",
          "insertions": 213,
          "deletions": 146,
          "lines_changed": 359
        },
        {
          "path": "templates/settings_2fa.php",
          "insertions": 96,
          "deletions": 0,
          "lines_changed": 96
        },
        {
          "path": "templates/settings_email.php",
          "insertions": 74,
          "deletions": 0,
          "lines_changed": 74
        },
        {
          "path": "templates/timeline.php",
          "insertions": 310,
          "deletions": 0,
          "lines_changed": 310
        },
        {
          "path": "templates/trends.php",
          "insertions": 14,
          "deletions": 12,
          "lines_changed": 26
        },
        {
          "path": "templates/yearly.php",
          "insertions": 30,
          "deletions": 32,
          "lines_changed": 62
        },
        {
          "path": "tests/Domain/BackupCodesTest.php",
          "insertions": 42,
          "deletions": 0,
          "lines_changed": 42
        },
        {
          "path": "tests/Domain/BadgeInputTest.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "tests/Domain/BadgesTimelineTest.php",
          "insertions": 210,
          "deletions": 13,
          "lines_changed": 223
        },
        {
          "path": "tests/Domain/Content/LocalizedQuoteTextTest.php",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "tests/Domain/CumulativeBadgesTest.php",
          "insertions": 107,
          "deletions": 0,
          "lines_changed": 107
        },
        {
          "path": "tests/Domain/DayOverDayDeltaTest.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "tests/Domain/DecimalInputTest.php",
          "insertions": 75,
          "deletions": 0,
          "lines_changed": 75
        },
        {
          "path": "tests/Domain/EmailChangeTest.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "tests/Domain/EventInputTest.php",
          "insertions": 68,
          "deletions": 0,
          "lines_changed": 68
        },
        {
          "path": "tests/Domain/GaloisFieldTest.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "tests/Domain/GearAutonomyTest.php",
          "insertions": 116,
          "deletions": 0,
          "lines_changed": 116
        },
        {
          "path": "tests/Domain/GearBatteryTest.php",
          "insertions": 39,
          "deletions": 26,
          "lines_changed": 65
        },
        {
          "path": "tests/Domain/GearInputTest.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "tests/Domain/GearPickerOrderTest.php",
          "insertions": 63,
          "deletions": 0,
          "lines_changed": 63
        },
        {
          "path": "tests/Domain/NewestQuotesTest.php",
          "insertions": 92,
          "deletions": 0,
          "lines_changed": 92
        },
        {
          "path": "tests/Domain/Notifications/NotificationBulkSelectionTest.php",
          "insertions": 64,
          "deletions": 0,
          "lines_changed": 64
        },
        {
          "path": "tests/Domain/OptionalUrlTest.php",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        },
        {
          "path": "tests/Domain/PaginationTest.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "tests/Domain/QrEncoderTest.php",
          "insertions": 76,
          "deletions": 0,
          "lines_changed": 76
        },
        {
          "path": "tests/Domain/QrMaskPenaltyTest.php",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "tests/Domain/QuoteInputTest.php",
          "insertions": 8,
          "deletions": 3,
          "lines_changed": 11
        },
        {
          "path": "tests/Domain/RaceReactivationTest.php",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "tests/Domain/RaceRegistrationStatusTest.php",
          "insertions": 43,
          "deletions": 0,
          "lines_changed": 43
        },
        {
          "path": "tests/Domain/RecentlyEarnedBadgesTest.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "tests/Domain/ReedSolomonEncoderTest.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "tests/Domain/RouteSegmentNoveltyTest.php",
          "insertions": 81,
          "deletions": 1,
          "lines_changed": 82
        },
        {
          "path": "tests/Domain/RouteThumbnailTest.php",
          "insertions": 125,
          "deletions": 0,
          "lines_changed": 125
        },
        {
          "path": "tests/Domain/SessionPartnersInputTest.php",
          "insertions": 86,
          "deletions": 0,
          "lines_changed": 86
        },
        {
          "path": "tests/Domain/SessionSearchQueryTest.php",
          "insertions": 163,
          "deletions": 0,
          "lines_changed": 163
        },
        {
          "path": "tests/Domain/StepUpAuthTest.php",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "tests/Domain/TagCloudTest.php",
          "insertions": 83,
          "deletions": 0,
          "lines_changed": 83
        },
        {
          "path": "tests/Domain/TimelineTest.php",
          "insertions": 116,
          "deletions": 0,
          "lines_changed": 116
        },
        {
          "path": "tests/Domain/TotpTest.php",
          "insertions": 81,
          "deletions": 0,
          "lines_changed": 81
        },
        {
          "path": "tests/Domain/TwoFactorRecoveryOtpTest.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "tests/Http/Records/BadgeCatalogLocalizationTest.php",
          "insertions": 49,
          "deletions": 0,
          "lines_changed": 49
        },
        {
          "path": "tests/Http/Records/BadgeCatalogTest.php",
          "insertions": 154,
          "deletions": 2,
          "lines_changed": 156
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 126,
          "deletions": 0,
          "lines_changed": 126
        },
        {
          "path": "tests/Http/Sessions/SessionSearchSqlTest.php",
          "insertions": 87,
          "deletions": 0,
          "lines_changed": 87
        },
        {
          "path": "tests/Infra/I18n/DateLabelsTest.php",
          "insertions": 80,
          "deletions": 0,
          "lines_changed": 80
        },
        {
          "path": "tests/Infra/I18n/LocaleTest.php",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "tests/Infra/I18n/NotificationsCatalogTest.php",
          "insertions": 60,
          "deletions": 0,
          "lines_changed": 60
        },
        {
          "path": "tests/Infra/I18n/TranslatorTest.php",
          "insertions": 55,
          "deletions": 0,
          "lines_changed": 55
        },
        {
          "path": "tests/Infra/Security/TotpSecretCipherTest.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        }
      ],
      "files_changed": 306,
      "insertions": 30405,
      "deletions": 2808
    },
    {
      "hash": "d0e043611ee921a2e5d4e7e95750a1be8c7e2255",
      "short_hash": "d0e0436",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787871052,
      "message": "CI/CD - Workflow de deploiement complet : config.local.php generee depuis les secrets, FTP, seed de demo (SPOR-595)\n\nReecrit .github/workflows/deploy.yml (tag v* + workflow_dispatch, noms de\nsecrets alignes sur le depot, extensions gd/sodium ajoutees a setup-php) :\ntests -> build --no-dev -> generation de config/config.local.php ->\nFTP differentiel -> migrate.php -> healthcheck.php.\n\n- Infra\\Config\\ConfigLocalRenderer (pur, teste) + bin/render-config-local.php :\n  config.local.php ecrite depuis les variables d'env (secrets CI), validee\n  (OPS_TOKEN/TWO_FACTOR_KEY 64 hex, APP_ENV, secrets requis). Le fichier est\n  desormais deploye par FTP, plus depose a la main. Reste gitignore.\n- Seed de demo hors GitHub (prod injoignable en SSH/MySQL distant) :\n  bin/deploy-seed.sh (mysqldump base podman -> gzip -> FTP dans var/seed/\n  hors webroot -> appel seed.php) + bin/.env.deploy.dist.\n- public/seed.php : meme auth/anti-bruteforce que migrate.php (ops:<ip>,\n  LoginThrottle, sleep(1), SecurityLogger), &confirm=erase obligatoire,\n  validation Infra\\Db\\SeedDump (pur, teste : magic gzip, plafond taille,\n  rejet CREATE/DROP DATABASE, USE, LOAD DATA), import PDO multi-statements,\n  suppression du dump apres import.\n- docs/deploiement.md (guide self-hosting), .gitignore (var/seed/, bin/.env.deploy).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "CI/CD - Workflow de deploiement complet : config.local.php generee depuis les secrets, FTP, seed de ",
      "type": "ops",
      "modified_files": [
        ".github/workflows/deploy.yml",
        ".gitignore",
        "bin/.env.deploy.dist",
        "bin/deploy-seed.sh",
        "bin/render-config-local.php",
        "docs/deploiement.md",
        "public/seed.php",
        "src/Infra/Config/ConfigLocalRenderer.php",
        "src/Infra/Db/SeedDump.php",
        "tests/Infra/Config/ConfigLocalRendererTest.php",
        "tests/Infra/Db/SeedDumpTest.php"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 60,
          "deletions": 12,
          "lines_changed": 72
        },
        {
          "path": ".gitignore",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "bin/.env.deploy.dist",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "bin/deploy-seed.sh",
          "insertions": 91,
          "deletions": 0,
          "lines_changed": 91
        },
        {
          "path": "bin/render-config-local.php",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        },
        {
          "path": "docs/deploiement.md",
          "insertions": 91,
          "deletions": 0,
          "lines_changed": 91
        },
        {
          "path": "public/seed.php",
          "insertions": 148,
          "deletions": 0,
          "lines_changed": 148
        },
        {
          "path": "src/Infra/Config/ConfigLocalRenderer.php",
          "insertions": 110,
          "deletions": 0,
          "lines_changed": 110
        },
        {
          "path": "src/Infra/Db/SeedDump.php",
          "insertions": 76,
          "deletions": 0,
          "lines_changed": 76
        },
        {
          "path": "tests/Infra/Config/ConfigLocalRendererTest.php",
          "insertions": 106,
          "deletions": 0,
          "lines_changed": 106
        },
        {
          "path": "tests/Infra/Db/SeedDumpTest.php",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        }
      ],
      "files_changed": 11,
      "insertions": 803,
      "deletions": 12
    },
    {
      "hash": "02c5ded7ac56712068390b7be823f37de297da42",
      "short_hash": "02c5ded",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787157297,
      "message": "Objectifs Yearly - Pastille sidebar : exclure les objectifs \"Planned\" du compte (SPOR-504)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Objectifs Yearly - Pastille sidebar : exclure les objectifs \"Planned\" du compte (SPOR-504)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/objectifs.md",
        "src/Http/Goals/YearlySidebarCounter.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Goals/YearlySidebarCounter.php",
          "insertions": 15,
          "deletions": 1,
          "lines_changed": 16
        }
      ],
      "files_changed": 4,
      "insertions": 53,
      "deletions": 1
    },
    {
      "hash": "251a42ef3c396a6bafb402ca28f1a54258d7e93b",
      "short_hash": "251a42e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787131666,
      "message": "Fixtures References/dataset/*.fit anonymisees (GPS translate vers la Laponie avec rescale cos(lat), serial_number neutralise, CRC recalcule) avant commit, meme politique que les fixtures TCX/FIT existantes\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Fixtures References/dataset/*.fit anonymisees (GPS translate vers la Laponie avec rescale cos(lat), ",
      "type": "fix",
      "modified_files": [
        "References/dataset/ok-23963800999_ACTIVITY.fit",
        "References/dataset/trace-nok24024654466_ACTIVITY.fit"
      ],
      "modified_files_details": [
        {
          "path": "References/dataset/ok-23963800999_ACTIVITY.fit",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "References/dataset/trace-nok24024654466_ACTIVITY.fit",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        }
      ],
      "files_changed": 2,
      "insertions": 0,
      "deletions": 0
    },
    {
      "hash": "f6b333ed1638b4a070ff8eeffff2c4c57bc27406",
      "short_hash": "f6b333e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787131077,
      "message": "Badges - Timeline des badges gagnes et carousel dashboard (SPOR-499/500) ; Materiel - Exclusion de la rotation suggeree + accordeon sous-categories/onglet Sports (SPOR-502/503)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Badges - Timeline des badges gagnes et carousel dashboard (SPOR-499/500) ; Materiel - Exclusion de l",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "References/design/assets/css/components.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/110_add_gear_include_in_suggestions.sql",
        "docs/badges.md",
        "docs/materiel.md",
        "public/assets/css/app.css",
        "public/assets/css/components.css",
        "public/assets/js/dashboard-badge-carousel.js",
        "src/Domain/Training/BadgesTimeline.php",
        "src/Domain/Training/GearInput.php",
        "src/Domain/Training/RecentlyEarnedBadges.php",
        "src/Http/Gear/GearController.php",
        "src/Http/Gear/GearSuggestions.php",
        "src/Http/Records/BadgeCatalog.php",
        "src/Http/Records/BadgesController.php",
        "templates/badges.php",
        "templates/partials/gear_edit_modal.php",
        "templates/partials/gear_form_fields.php",
        "tests/Domain/BadgesTimelineTest.php",
        "tests/Domain/GearInputTest.php",
        "tests/Domain/RecentlyEarnedBadgesTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "References/design/assets/css/components.css",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 150,
          "deletions": 0,
          "lines_changed": 150
        },
        {
          "path": "db/migrations/110_add_gear_include_in_suggestions.sql",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "docs/badges.md",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "docs/materiel.md",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "public/assets/js/dashboard-badge-carousel.js",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "src/Domain/Training/BadgesTimeline.php",
          "insertions": 111,
          "deletions": 0,
          "lines_changed": 111
        },
        {
          "path": "src/Domain/Training/GearInput.php",
          "insertions": 19,
          "deletions": 1,
          "lines_changed": 20
        },
        {
          "path": "src/Domain/Training/RecentlyEarnedBadges.php",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 25,
          "deletions": 6,
          "lines_changed": 31
        },
        {
          "path": "src/Http/Gear/GearSuggestions.php",
          "insertions": 10,
          "deletions": 2,
          "lines_changed": 12
        },
        {
          "path": "src/Http/Records/BadgeCatalog.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "src/Http/Records/BadgesController.php",
          "insertions": 37,
          "deletions": 32,
          "lines_changed": 69
        },
        {
          "path": "templates/badges.php",
          "insertions": 134,
          "deletions": 2,
          "lines_changed": 136
        },
        {
          "path": "templates/partials/gear_edit_modal.php",
          "insertions": 8,
          "deletions": 1,
          "lines_changed": 9
        },
        {
          "path": "templates/partials/gear_form_fields.php",
          "insertions": 124,
          "deletions": 68,
          "lines_changed": 192
        },
        {
          "path": "tests/Domain/BadgesTimelineTest.php",
          "insertions": 100,
          "deletions": 0,
          "lines_changed": 100
        },
        {
          "path": "tests/Domain/GearInputTest.php",
          "insertions": 22,
          "deletions": 0,
          "lines_changed": 22
        },
        {
          "path": "tests/Domain/RecentlyEarnedBadgesTest.php",
          "insertions": 88,
          "deletions": 0,
          "lines_changed": 88
        }
      ],
      "files_changed": 23,
      "insertions": 1020,
      "deletions": 118
    },
    {
      "hash": "36c1e0fa4a81a302e4f020d6309c9a6f4cee6443",
      "short_hash": "36c1e0f",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787124441,
      "message": "Citations - Pool dashboard personnalise (publiques + privees) et pondere par les votes de l'utilisateur (addendum SPOR-501)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Citations - Pool dashboard personnalise (publiques + privees) et pondere par les votes de l'utilisat",
      "type": "other",
      "modified_files": [
        "TECHNICAL.md",
        "docs/dashboard.md",
        "src/Domain/Content/QuoteOfTheDay.php",
        "src/Http/Dashboard/DashboardController.php",
        "templates/dashboard.php",
        "tests/Domain/QuoteOfTheDayTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 76,
          "deletions": 0,
          "lines_changed": 76
        },
        {
          "path": "docs/dashboard.md",
          "insertions": 8,
          "deletions": 2,
          "lines_changed": 10
        },
        {
          "path": "src/Domain/Content/QuoteOfTheDay.php",
          "insertions": 45,
          "deletions": 12,
          "lines_changed": 57
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 47,
          "deletions": 21,
          "lines_changed": 68
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 23,
          "deletions": 7,
          "lines_changed": 30
        },
        {
          "path": "tests/Domain/QuoteOfTheDayTest.php",
          "insertions": 102,
          "deletions": 27,
          "lines_changed": 129
        }
      ],
      "files_changed": 6,
      "insertions": 301,
      "deletions": 69
    },
    {
      "hash": "2a83185ccbdcb9647d9e48408cbd34c8f5e5a458",
      "short_hash": "2a83185",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787112057,
      "message": "Citations - Fix confidentialite (admin ne voit plus les quotes privees) + moderation avant rotation dashboard (addendum SPOR-501)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Citations - Fix confidentialite (admin ne voit plus les quotes privees) + moderation avant rotation ",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "docs/admin.md",
        "docs/quotes.md",
        "src/Http/Admin/AdminQuotesController.php",
        "src/Http/Content/QuotesController.php",
        "src/Http/Dashboard/DashboardController.php",
        "templates/admin_quotes.php",
        "templates/dashboard.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 72,
          "deletions": 3,
          "lines_changed": 75
        },
        {
          "path": "docs/admin.md",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "docs/quotes.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/AdminQuotesController.php",
          "insertions": 23,
          "deletions": 4,
          "lines_changed": 27
        },
        {
          "path": "src/Http/Content/QuotesController.php",
          "insertions": 18,
          "deletions": 4,
          "lines_changed": 22
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 14,
          "deletions": 3,
          "lines_changed": 17
        },
        {
          "path": "templates/admin_quotes.php",
          "insertions": 12,
          "deletions": 3,
          "lines_changed": 15
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 14,
          "deletions": 6,
          "lines_changed": 20
        }
      ],
      "files_changed": 8,
      "insertions": 166,
      "deletions": 23
    },
    {
      "hash": "0a11d777e15ada297af747f4beb8e9da33883101",
      "short_hash": "0a11d77",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787094643,
      "message": "Citations - Date d'ajout affichee + vote/copie depuis le dashboard (addendum SPOR-501)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Citations - Date d'ajout affichee + vote/copie depuis le dashboard (addendum SPOR-501)",
      "type": "feat",
      "modified_files": [
        "TECHNICAL.md",
        "docs/dashboard.md",
        "src/Http/Content/QuotesController.php",
        "src/Http/Dashboard/DashboardController.php",
        "templates/dashboard.php",
        "templates/partials/quote_public_card.php",
        "templates/quotes.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 80,
          "deletions": 14,
          "lines_changed": 94
        },
        {
          "path": "docs/dashboard.md",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        },
        {
          "path": "src/Http/Content/QuotesController.php",
          "insertions": 41,
          "deletions": 17,
          "lines_changed": 58
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 98,
          "deletions": 4,
          "lines_changed": 102
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 86,
          "deletions": 3,
          "lines_changed": 89
        },
        {
          "path": "templates/partials/quote_public_card.php",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "templates/quotes.php",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        }
      ],
      "files_changed": 7,
      "insertions": 318,
      "deletions": 43
    },
    {
      "hash": "a6e221548d101d488f983ead6b8544b902ee448e",
      "short_hash": "a6e2215",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787093626,
      "message": "Citations - Page /quotes : soumission utilisateur, vote +1/-1, collection privee (SPOR-501)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Citations - Page /quotes : soumission utilisateur, vote +1/-1, collection privee (SPOR-501)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/108_add_quotes_user_visibility.sql",
        "db/migrations/109_add_quote_votes.sql",
        "docs/README.md",
        "docs/quotes.md",
        "src/Domain/Content/QuoteDuplicateCheck.php",
        "src/Domain/Content/QuoteInput.php",
        "src/Http/Content/QuotesController.php",
        "templates/partials/app_sidebar.php",
        "templates/partials/quote_private_edit_modal.php",
        "templates/partials/quote_public_card.php",
        "templates/quotes.php",
        "tests/Domain/QuoteDuplicateCheckTest.php",
        "tests/Domain/QuoteInputTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 337,
          "deletions": 0,
          "lines_changed": 337
        },
        {
          "path": "config/routes.php",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "db/migrations/108_add_quotes_user_visibility.sql",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "db/migrations/109_add_quote_votes.sql",
          "insertions": 22,
          "deletions": 0,
          "lines_changed": 22
        },
        {
          "path": "docs/README.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "docs/quotes.md",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "src/Domain/Content/QuoteDuplicateCheck.php",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "src/Domain/Content/QuoteInput.php",
          "insertions": 68,
          "deletions": 0,
          "lines_changed": 68
        },
        {
          "path": "src/Http/Content/QuotesController.php",
          "insertions": 368,
          "deletions": 0,
          "lines_changed": 368
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "templates/partials/quote_private_edit_modal.php",
          "insertions": 76,
          "deletions": 0,
          "lines_changed": 76
        },
        {
          "path": "templates/partials/quote_public_card.php",
          "insertions": 48,
          "deletions": 0,
          "lines_changed": 48
        },
        {
          "path": "templates/quotes.php",
          "insertions": 156,
          "deletions": 0,
          "lines_changed": 156
        },
        {
          "path": "tests/Domain/QuoteDuplicateCheckTest.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "tests/Domain/QuoteInputTest.php",
          "insertions": 80,
          "deletions": 0,
          "lines_changed": 80
        }
      ],
      "files_changed": 16,
      "insertions": 1318,
      "deletions": 2
    },
    {
      "hash": "ef5e4b0e3596b3d1e2362959543aaa18b0d608bc",
      "short_hash": "ef5e4b0",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787092254,
      "message": "Fuseau horaire configurable - Reglage par profil + detection GPS automatique par seance (SPOR-495/496/497)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Fuseau horaire configurable - Reglage par profil + detection GPS automatique par seance (SPOR-495/49",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/107_add_users_timezone.sql",
        "docs/profil.md",
        "docs/sessions.md",
        "src/Domain/Training/TimezoneLookup.php",
        "src/Domain/Training/TimezoneReferencePoints.php",
        "src/Http/Sessions/ImportedTimeFormatter.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionTimezoneResolver.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Http/Settings/SettingsController.php",
        "templates/settings.php",
        "tests/Domain/TimezoneLookupTest.php",
        "tests/Http/Sessions/ImportedTimeFormatterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 5,
          "deletions": 3,
          "lines_changed": 8
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 93,
          "deletions": 0,
          "lines_changed": 93
        },
        {
          "path": "db/migrations/107_add_users_timezone.sql",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "docs/profil.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Domain/Training/TimezoneLookup.php",
          "insertions": 49,
          "deletions": 0,
          "lines_changed": 49
        },
        {
          "path": "src/Domain/Training/TimezoneReferencePoints.php",
          "insertions": 340,
          "deletions": 0,
          "lines_changed": 340
        },
        {
          "path": "src/Http/Sessions/ImportedTimeFormatter.php",
          "insertions": 9,
          "deletions": 7,
          "lines_changed": 16
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 18,
          "deletions": 10,
          "lines_changed": 28
        },
        {
          "path": "src/Http/Sessions/SessionTimezoneResolver.php",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 8,
          "deletions": 3,
          "lines_changed": 11
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 22,
          "deletions": 3,
          "lines_changed": 25
        },
        {
          "path": "templates/settings.php",
          "insertions": 27,
          "deletions": 2,
          "lines_changed": 29
        },
        {
          "path": "tests/Domain/TimezoneLookupTest.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "tests/Http/Sessions/ImportedTimeFormatterTest.php",
          "insertions": 20,
          "deletions": 21,
          "lines_changed": 41
        }
      ],
      "files_changed": 15,
      "insertions": 689,
      "deletions": 49
    },
    {
      "hash": "9de190976e6755ea3d83d4ae4a9a9c93d7402a59",
      "short_hash": "9de1909",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787090769,
      "message": "Badges - Type distance domicile vers zone aurores boreales, calculee par utilisateur (SPOR-498, Revontulet)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Badges - Type distance domicile vers zone aurores boreales, calculee par utilisateur (SPOR-498, Revo",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/106_add_badge_geo_home_distance_type.sql",
        "docs/badges.md",
        "src/Domain/Training/AuroraDistanceBadge.php",
        "src/Domain/Training/BadgeInput.php",
        "src/Http/Admin/AdminBadgesController.php",
        "src/Http/Notifications/NotificationGenerator.php",
        "src/Http/Records/BadgeCatalog.php",
        "src/Http/Records/BadgesController.php",
        "templates/partials/badge_form_fields.php",
        "tests/Domain/AuroraDistanceBadgeTest.php",
        "tests/Domain/BadgeInputTest.php",
        "tests/Http/Records/BadgeCatalogTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 88,
          "deletions": 0,
          "lines_changed": 88
        },
        {
          "path": "db/migrations/106_add_badge_geo_home_distance_type.sql",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "docs/badges.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "src/Domain/Training/AuroraDistanceBadge.php",
          "insertions": 85,
          "deletions": 0,
          "lines_changed": 85
        },
        {
          "path": "src/Domain/Training/BadgeInput.php",
          "insertions": 16,
          "deletions": 1,
          "lines_changed": 17
        },
        {
          "path": "src/Http/Admin/AdminBadgesController.php",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "src/Http/Notifications/NotificationGenerator.php",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "src/Http/Records/BadgeCatalog.php",
          "insertions": 44,
          "deletions": 1,
          "lines_changed": 45
        },
        {
          "path": "src/Http/Records/BadgesController.php",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "templates/partials/badge_form_fields.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "tests/Domain/AuroraDistanceBadgeTest.php",
          "insertions": 92,
          "deletions": 0,
          "lines_changed": 92
        },
        {
          "path": "tests/Domain/BadgeInputTest.php",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "tests/Http/Records/BadgeCatalogTest.php",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        }
      ],
      "files_changed": 14,
      "insertions": 430,
      "deletions": 6
    },
    {
      "hash": "585dddb8f8157ddb59d1b007d55a6524687e9c8c",
      "short_hash": "585dddb",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787089802,
      "message": "Nettoyage - Suppression du stand-in SVG viz.js mort dans l'app deployee (plus charge par aucun template depuis SPOR-312/313)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Nettoyage - Suppression du stand-in SVG viz.js mort dans l'app deployee (plus charge par aucun templ",
      "type": "ops",
      "modified_files": [
        "TECHNICAL.md",
        "public/assets/css/components.css",
        "public/assets/js/viz.js",
        "src/Http/Sessions/SessionPresenter.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "public/assets/js/viz.js",
          "insertions": 0,
          "deletions": 137,
          "lines_changed": 137
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 7,
          "deletions": 8,
          "lines_changed": 15
        }
      ],
      "files_changed": 4,
      "insertions": 36,
      "deletions": 147
    },
    {
      "hash": "6adc4bee029397aa1e96927dca1640b13de1f6d2",
      "short_hash": "6adc4be",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1787089560,
      "message": "Materiel - Batterie (SPOR-466 a 477/482/485/489), usure/entretien (SPOR-449 a 452/463), multi-sport (SPOR-472/478 a 483) ; Sessions - Fusion d'enregistrements (SPOR-486 a 494) et fuseau horaire a l'import (SPOR-494)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Materiel - Batterie (SPOR-466 a 477/482/485/489), usure/entretien (SPOR-449 a 452/463), multi-sport ",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "TODO.md",
        "config/routes.php",
        "db/migrations/096_add_gear_service_log_enabled.sql",
        "db/migrations/097_add_gear_url.sql",
        "db/migrations/098_add_gear_media_cover.sql",
        "db/migrations/099_add_gear_has_battery.sql",
        "db/migrations/100_add_gear_battery_entries.sql",
        "db/migrations/101_add_gear_sports.sql",
        "db/migrations/102_add_gear_subsports_sport.sql",
        "db/migrations/103_add_gear_batteries.sql",
        "db/migrations/104_add_gear_battery_entries_battery_id.sql",
        "db/migrations/105_add_gear_sports_match_any_subsport.sql",
        "docs/dashboard.md",
        "docs/materiel.md",
        "docs/sessions.md",
        "public/assets/css/app.css",
        "public/assets/css/components.css",
        "public/assets/js/gear-battery-chart.js",
        "public/assets/js/icons.js",
        "public/assets/js/interactions.js",
        "src/Domain/Import/TrackpointSplice.php",
        "src/Domain/Training/GearAutoAssignMatcher.php",
        "src/Domain/Training/GearBattery.php",
        "src/Domain/Training/GearBatteryEntryInput.php",
        "src/Domain/Training/GearInput.php",
        "src/Domain/Training/SessionMergeSelection.php",
        "src/Domain/Training/TcxWriter.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Gear/GearAutoAssignController.php",
        "src/Http/Gear/GearAutoAssignMap.php",
        "src/Http/Gear/GearBatteryController.php",
        "src/Http/Gear/GearBatteryStore.php",
        "src/Http/Gear/GearController.php",
        "src/Http/Gear/GearMediaController.php",
        "src/Http/Gear/GearMediaStore.php",
        "src/Http/Gear/GearOptionLabels.php",
        "src/Http/Gear/GearSuggestions.php",
        "src/Http/Sessions/ImportedTimeFormatter.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionMergeStore.php",
        "src/Http/Sessions/SessionPresenter.php",
        "src/Http/Sessions/SessionsController.php",
        "templates/dashboard.php",
        "templates/gear.php",
        "templates/gear_auto_assign_review.php",
        "templates/gear_detail.php",
        "templates/partials/gear_battery_modal.php",
        "templates/partials/gear_edit_modal.php",
        "templates/partials/gear_form_fields.php",
        "templates/session_detail.php",
        "templates/sessions.php",
        "tests/Domain/GearAutoAssignMatcherTest.php",
        "tests/Domain/GearBatteryEntryInputTest.php",
        "tests/Domain/GearBatteryTest.php",
        "tests/Domain/GearInputTest.php",
        "tests/Domain/SessionMergeSelectionTest.php",
        "tests/Domain/TcxWriterTest.php",
        "tests/Domain/TrackpointSpliceTest.php",
        "tests/Http/Sessions/ImportedTimeFormatterTest.php",
        "tests/Http/Sessions/SessionPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 62,
          "deletions": 4,
          "lines_changed": 66
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 2083,
          "deletions": 2,
          "lines_changed": 2085
        },
        {
          "path": "TODO.md",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "config/routes.php",
          "insertions": 19,
          "deletions": 1,
          "lines_changed": 20
        },
        {
          "path": "db/migrations/096_add_gear_service_log_enabled.sql",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "db/migrations/097_add_gear_url.sql",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "db/migrations/098_add_gear_media_cover.sql",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "db/migrations/099_add_gear_has_battery.sql",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "db/migrations/100_add_gear_battery_entries.sql",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "db/migrations/101_add_gear_sports.sql",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "db/migrations/102_add_gear_subsports_sport.sql",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "db/migrations/103_add_gear_batteries.sql",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "db/migrations/104_add_gear_battery_entries_battery_id.sql",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "db/migrations/105_add_gear_sports_match_any_subsport.sql",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "docs/dashboard.md",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "docs/materiel.md",
          "insertions": 197,
          "deletions": 6,
          "lines_changed": 203
        },
        {
          "path": "docs/sessions.md",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "public/assets/js/gear-battery-chart.js",
          "insertions": 87,
          "deletions": 0,
          "lines_changed": 87
        },
        {
          "path": "public/assets/js/icons.js",
          "insertions": 38,
          "deletions": 1,
          "lines_changed": 39
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 167,
          "deletions": 35,
          "lines_changed": 202
        },
        {
          "path": "src/Domain/Import/TrackpointSplice.php",
          "insertions": 233,
          "deletions": 0,
          "lines_changed": 233
        },
        {
          "path": "src/Domain/Training/GearAutoAssignMatcher.php",
          "insertions": 47,
          "deletions": 6,
          "lines_changed": 53
        },
        {
          "path": "src/Domain/Training/GearBattery.php",
          "insertions": 160,
          "deletions": 0,
          "lines_changed": 160
        },
        {
          "path": "src/Domain/Training/GearBatteryEntryInput.php",
          "insertions": 125,
          "deletions": 0,
          "lines_changed": 125
        },
        {
          "path": "src/Domain/Training/GearInput.php",
          "insertions": 233,
          "deletions": 30,
          "lines_changed": 263
        },
        {
          "path": "src/Domain/Training/SessionMergeSelection.php",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "src/Domain/Training/TcxWriter.php",
          "insertions": 147,
          "deletions": 0,
          "lines_changed": 147
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 75,
          "deletions": 2,
          "lines_changed": 77
        },
        {
          "path": "src/Http/Gear/GearAutoAssignController.php",
          "insertions": 65,
          "deletions": 11,
          "lines_changed": 76
        },
        {
          "path": "src/Http/Gear/GearAutoAssignMap.php",
          "insertions": 27,
          "deletions": 8,
          "lines_changed": 35
        },
        {
          "path": "src/Http/Gear/GearBatteryController.php",
          "insertions": 303,
          "deletions": 0,
          "lines_changed": 303
        },
        {
          "path": "src/Http/Gear/GearBatteryStore.php",
          "insertions": 267,
          "deletions": 0,
          "lines_changed": 267
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 652,
          "deletions": 87,
          "lines_changed": 739
        },
        {
          "path": "src/Http/Gear/GearMediaController.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "src/Http/Gear/GearMediaStore.php",
          "insertions": 72,
          "deletions": 13,
          "lines_changed": 85
        },
        {
          "path": "src/Http/Gear/GearOptionLabels.php",
          "insertions": 64,
          "deletions": 0,
          "lines_changed": 64
        },
        {
          "path": "src/Http/Gear/GearSuggestions.php",
          "insertions": 23,
          "deletions": 10,
          "lines_changed": 33
        },
        {
          "path": "src/Http/Sessions/ImportedTimeFormatter.php",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 746,
          "deletions": 10,
          "lines_changed": 756
        },
        {
          "path": "src/Http/Sessions/SessionMergeStore.php",
          "insertions": 95,
          "deletions": 0,
          "lines_changed": 95
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 86,
          "deletions": 7,
          "lines_changed": 93
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 26,
          "deletions": 10,
          "lines_changed": 36
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "templates/gear.php",
          "insertions": 157,
          "deletions": 25,
          "lines_changed": 182
        },
        {
          "path": "templates/gear_auto_assign_review.php",
          "insertions": 32,
          "deletions": 2,
          "lines_changed": 34
        },
        {
          "path": "templates/gear_detail.php",
          "insertions": 185,
          "deletions": 31,
          "lines_changed": 216
        },
        {
          "path": "templates/partials/gear_battery_modal.php",
          "insertions": 187,
          "deletions": 0,
          "lines_changed": 187
        },
        {
          "path": "templates/partials/gear_edit_modal.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "templates/partials/gear_form_fields.php",
          "insertions": 193,
          "deletions": 36,
          "lines_changed": 229
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 426,
          "deletions": 28,
          "lines_changed": 454
        },
        {
          "path": "templates/sessions.php",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "tests/Domain/GearAutoAssignMatcherTest.php",
          "insertions": 123,
          "deletions": 0,
          "lines_changed": 123
        },
        {
          "path": "tests/Domain/GearBatteryEntryInputTest.php",
          "insertions": 149,
          "deletions": 0,
          "lines_changed": 149
        },
        {
          "path": "tests/Domain/GearBatteryTest.php",
          "insertions": 217,
          "deletions": 0,
          "lines_changed": 217
        },
        {
          "path": "tests/Domain/GearInputTest.php",
          "insertions": 420,
          "deletions": 13,
          "lines_changed": 433
        },
        {
          "path": "tests/Domain/SessionMergeSelectionTest.php",
          "insertions": 115,
          "deletions": 0,
          "lines_changed": 115
        },
        {
          "path": "tests/Domain/TcxWriterTest.php",
          "insertions": 106,
          "deletions": 0,
          "lines_changed": 106
        },
        {
          "path": "tests/Domain/TrackpointSpliceTest.php",
          "insertions": 204,
          "deletions": 0,
          "lines_changed": 204
        },
        {
          "path": "tests/Http/Sessions/ImportedTimeFormatterTest.php",
          "insertions": 52,
          "deletions": 0,
          "lines_changed": 52
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 73,
          "deletions": 0,
          "lines_changed": 73
        }
      ],
      "files_changed": 63,
      "insertions": 9208,
      "deletions": 379
    },
    {
      "hash": "f77e07c12e9cec9bbaa680aa49859d71a38ede72",
      "short_hash": "f77e07c",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786920141,
      "message": "Badges - Type \"plage horaire\", deux bornes configurables par badge (SPOR-462)\n\nNouveau type de badge time_range : cumule le nombre de seances demarrees\ndans une fenetre [range_start_time, range_end_time[, avec repli sur le\nlendemain si la borne de fin est plus tot que la borne de debut (ex.\n22:00-02:00). Reutilise threshold_time (SPOR-459) comme debut de plage,\najoute threshold_time_end comme fin.\n\nRemplace le type_after \"Mogouai\" envisage initialement : un seuil \"apres\nminuit\" est vrai pour quasiment toute la journee et n'aurait donc rien\nrecompense de particulier. Badge \"Mogouai\" cree en donnees (00:00-01:00).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Badges - Type \"plage horaire\", deux bornes configurables par badge (SPOR-462)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/095_add_badge_time_range.sql",
        "docs/badges.md",
        "src/Domain/Training/BadgeInput.php",
        "src/Http/Admin/AdminBadgesController.php",
        "src/Http/Notifications/NotificationGenerator.php",
        "src/Http/Records/BadgeCatalog.php",
        "src/Http/Records/BadgesController.php",
        "templates/partials/badge_form_fields.php",
        "tests/Domain/BadgeInputTest.php",
        "tests/Http/Records/BadgeCatalogTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 57,
          "deletions": 0,
          "lines_changed": 57
        },
        {
          "path": "db/migrations/095_add_badge_time_range.sql",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "docs/badges.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "src/Domain/Training/BadgeInput.php",
          "insertions": 68,
          "deletions": 11,
          "lines_changed": 79
        },
        {
          "path": "src/Http/Admin/AdminBadgesController.php",
          "insertions": 28,
          "deletions": 7,
          "lines_changed": 35
        },
        {
          "path": "src/Http/Notifications/NotificationGenerator.php",
          "insertions": 5,
          "deletions": 5,
          "lines_changed": 10
        },
        {
          "path": "src/Http/Records/BadgeCatalog.php",
          "insertions": 45,
          "deletions": 16,
          "lines_changed": 61
        },
        {
          "path": "src/Http/Records/BadgesController.php",
          "insertions": 6,
          "deletions": 6,
          "lines_changed": 12
        },
        {
          "path": "templates/partials/badge_form_fields.php",
          "insertions": 29,
          "deletions": 0,
          "lines_changed": 29
        },
        {
          "path": "tests/Domain/BadgeInputTest.php",
          "insertions": 108,
          "deletions": 0,
          "lines_changed": 108
        },
        {
          "path": "tests/Http/Records/BadgeCatalogTest.php",
          "insertions": 63,
          "deletions": 2,
          "lines_changed": 65
        }
      ],
      "files_changed": 12,
      "insertions": 431,
      "deletions": 48
    },
    {
      "hash": "ef671c67242cfc257aba937805d09fa0a6532a20",
      "short_hash": "ef671c6",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786919072,
      "message": "Badges - Type \"apres une heure\", seuil configurable par badge (SPOR-461)\n\nNouveau type de badge time_after, symetrique de time_before (SPOR-459) :\ncumule le nombre de seances demarrees APRES une heure limite HH:MM\nconfigurable par l'admin (comparaison naive, meme convention que le\nreste de l'app). Reutilise la colonne threshold_time existante (aucune\nmigration de schema pour le seuil, un badge n'a jamais qu'un seul type\nactif) - seul le CHECK constraint est mis a jour pour accepter le\nnouveau type.\n\nTroisieme story de l'epic SPOR-458, ajoutee apres coup a la demande\nexplicite de l'utilisateur (idee \"Mogouai\" de la mindmap, non couverte\npar Marmotte/Night owl).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Badges - Type \"apres une heure\", seuil configurable par badge (SPOR-461)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/094_add_badge_time_after_type.sql",
        "docs/badges.md",
        "src/Domain/Training/BadgeInput.php",
        "src/Http/Admin/AdminBadgesController.php",
        "src/Http/Notifications/NotificationGenerator.php",
        "src/Http/Records/BadgeCatalog.php",
        "src/Http/Records/BadgesController.php",
        "templates/partials/badge_form_fields.php",
        "tests/Domain/BadgeInputTest.php",
        "tests/Http/Records/BadgeCatalogTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 54,
          "deletions": 0,
          "lines_changed": 54
        },
        {
          "path": "db/migrations/094_add_badge_time_after_type.sql",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "docs/badges.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "src/Domain/Training/BadgeInput.php",
          "insertions": 18,
          "deletions": 6,
          "lines_changed": 24
        },
        {
          "path": "src/Http/Admin/AdminBadgesController.php",
          "insertions": 10,
          "deletions": 2,
          "lines_changed": 12
        },
        {
          "path": "src/Http/Notifications/NotificationGenerator.php",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "src/Http/Records/BadgeCatalog.php",
          "insertions": 14,
          "deletions": 2,
          "lines_changed": 16
        },
        {
          "path": "src/Http/Records/BadgesController.php",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "templates/partials/badge_form_fields.php",
          "insertions": 21,
          "deletions": 1,
          "lines_changed": 22
        },
        {
          "path": "tests/Domain/BadgeInputTest.php",
          "insertions": 67,
          "deletions": 0,
          "lines_changed": 67
        },
        {
          "path": "tests/Http/Records/BadgeCatalogTest.php",
          "insertions": 43,
          "deletions": 0,
          "lines_changed": 43
        }
      ],
      "files_changed": 12,
      "insertions": 249,
      "deletions": 18
    },
    {
      "hash": "268867d7d04bf5438fdb4c0fcabcbd5172fde8df",
      "short_hash": "268867d",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786917371,
      "message": "Badges - Type \"apres le coucher du soleil\" (Night owl), calcule depuis la position GPS de la seance (SPOR-460)\n\nCache le point de depart GPS de chaque seance a l'import\n(sessions.start_lat/start_lon, comme elevation_gain_m/training_load) avec\nbackfill pour l'historique existant, pour eviter de re-decoder tout\nl'historique TCX/GPX/FIT a chaque affichage de /badges. Ajoute un calcul\nde coucher de soleil en PHP pur (formule astronomique standard, aucun\nservice tiers, precis a quelques minutes pres) et un nouveau type de\nbadge after_sunset : cumule le nombre de seances GPS demarrees apres le\ncoucher du soleil calcule pour leur position de depart reelle - jamais\nune heure fixe. Seances sans GPS jamais eligibles.\n\nDerniere des deux stories de l'epic SPOR-458 (criteres horaires de\nbadges) - epic complet.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Badges - Type \"apres le coucher du soleil\" (Night owl), calcule depuis la position GPS de la seance ",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "bin/backfill_session_coordinates.php",
        "db/migrations/092_add_session_start_coordinates.sql",
        "db/migrations/093_add_badge_after_sunset_type.sql",
        "docs/badges.md",
        "public/backfill_session_coordinates.php",
        "src/Domain/Import/SessionData.php",
        "src/Domain/Training/BadgeInput.php",
        "src/Domain/Training/SunsetCalculator.php",
        "src/Http/Admin/AdminBadgesController.php",
        "src/Http/Notifications/NotificationGenerator.php",
        "src/Http/Records/BadgeCatalog.php",
        "src/Http/Records/BadgesController.php",
        "src/Http/Records/BadgesSidebarCounter.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Infra/Db/SessionCoordinatesBackfill.php",
        "templates/partials/badge_form_fields.php",
        "tests/Domain/BadgeInputTest.php",
        "tests/Domain/SessionDataTest.php",
        "tests/Domain/SunsetCalculatorTest.php",
        "tests/Http/Records/BadgeCatalogTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 132,
          "deletions": 0,
          "lines_changed": 132
        },
        {
          "path": "bin/backfill_session_coordinates.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "db/migrations/092_add_session_start_coordinates.sql",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "db/migrations/093_add_badge_after_sunset_type.sql",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "docs/badges.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "public/backfill_session_coordinates.php",
          "insertions": 96,
          "deletions": 0,
          "lines_changed": 96
        },
        {
          "path": "src/Domain/Import/SessionData.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "src/Domain/Training/BadgeInput.php",
          "insertions": 21,
          "deletions": 9,
          "lines_changed": 30
        },
        {
          "path": "src/Domain/Training/SunsetCalculator.php",
          "insertions": 81,
          "deletions": 0,
          "lines_changed": 81
        },
        {
          "path": "src/Http/Admin/AdminBadgesController.php",
          "insertions": 9,
          "deletions": 3,
          "lines_changed": 12
        },
        {
          "path": "src/Http/Notifications/NotificationGenerator.php",
          "insertions": 8,
          "deletions": 4,
          "lines_changed": 12
        },
        {
          "path": "src/Http/Records/BadgeCatalog.php",
          "insertions": 37,
          "deletions": 5,
          "lines_changed": 42
        },
        {
          "path": "src/Http/Records/BadgesController.php",
          "insertions": 16,
          "deletions": 8,
          "lines_changed": 24
        },
        {
          "path": "src/Http/Records/BadgesSidebarCounter.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 11,
          "deletions": 2,
          "lines_changed": 13
        },
        {
          "path": "src/Infra/Db/SessionCoordinatesBackfill.php",
          "insertions": 84,
          "deletions": 0,
          "lines_changed": 84
        },
        {
          "path": "templates/partials/badge_form_fields.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "tests/Domain/BadgeInputTest.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "tests/Domain/SessionDataTest.php",
          "insertions": 57,
          "deletions": 0,
          "lines_changed": 57
        },
        {
          "path": "tests/Domain/SunsetCalculatorTest.php",
          "insertions": 79,
          "deletions": 0,
          "lines_changed": 79
        },
        {
          "path": "tests/Http/Records/BadgeCatalogTest.php",
          "insertions": 31,
          "deletions": 2,
          "lines_changed": 33
        }
      ],
      "files_changed": 22,
      "insertions": 810,
      "deletions": 37
    },
    {
      "hash": "56c823d87fc26f14bceec013311323de301cd03b",
      "short_hash": "56c823d",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786916348,
      "message": "Badges - Type \"avant une heure\" (Marmotte), seuil configurable par badge (SPOR-459)\n\nNouveau type de badge time_before : cumule le nombre de seances demarrees\navant une heure limite HH:MM configurable par l'admin, meme mecanique que\nle type count existant avec un filtre horaire en plus (comparaison naive,\nmeme convention que le reste de l'app - un seul fuseau horaire pour\nl'instance auto-hebergee).\n\nPremiere des deux stories de l'epic SPOR-458 (criteres horaires de\nbadges) ; la seconde (Night owl, apres le coucher du soleil) reste a\nfaire - elle necessite d'abord de trancher comment mettre en cache le\npoint de depart GPS de chaque seance sans re-decoder tout l'historique a\nchaque affichage de /badges.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Badges - Type \"avant une heure\" (Marmotte), seuil configurable par badge (SPOR-459)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/091_add_badge_threshold_time.sql",
        "docs/badges.md",
        "src/Domain/Training/BadgeInput.php",
        "src/Http/Admin/AdminBadgesController.php",
        "src/Http/Notifications/NotificationGenerator.php",
        "src/Http/Records/BadgeCatalog.php",
        "src/Http/Records/BadgesController.php",
        "templates/partials/badge_form_fields.php",
        "tests/Domain/BadgeInputTest.php",
        "tests/Http/Records/BadgeCatalogTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 95,
          "deletions": 0,
          "lines_changed": 95
        },
        {
          "path": "db/migrations/091_add_badge_threshold_time.sql",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "docs/badges.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "src/Domain/Training/BadgeInput.php",
          "insertions": 42,
          "deletions": 8,
          "lines_changed": 50
        },
        {
          "path": "src/Http/Admin/AdminBadgesController.php",
          "insertions": 26,
          "deletions": 12,
          "lines_changed": 38
        },
        {
          "path": "src/Http/Notifications/NotificationGenerator.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "src/Http/Records/BadgeCatalog.php",
          "insertions": 41,
          "deletions": 10,
          "lines_changed": 51
        },
        {
          "path": "src/Http/Records/BadgesController.php",
          "insertions": 6,
          "deletions": 4,
          "lines_changed": 10
        },
        {
          "path": "templates/partials/badge_form_fields.php",
          "insertions": 24,
          "deletions": 2,
          "lines_changed": 26
        },
        {
          "path": "tests/Domain/BadgeInputTest.php",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        },
        {
          "path": "tests/Http/Records/BadgeCatalogTest.php",
          "insertions": 48,
          "deletions": 4,
          "lines_changed": 52
        }
      ],
      "files_changed": 12,
      "insertions": 384,
      "deletions": 43
    },
    {
      "hash": "c06065151574737aed596437dcc734dfdf545a66",
      "short_hash": "c060651",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786915213,
      "message": "Mindmap - Criteres horaires de badges (SPOR-458/459/460) + note Tour de Mars/Titanic\n\nCree l'epic SPOR-458 (Marmotte/Night owl) et ses deux stories filles a\npartir des idees de backlog personnel deja presentes dans la mindmap\n(demande explicite de l'utilisateur). Ajoute aussi une note sur Tour de\nMars et Titanic, deja ajoutes au catalogue de badges via l'admin (pure\nconfiguration, sans story Jira).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Mindmap - Criteres horaires de badges (SPOR-458/459/460) + note Tour de Mars/Titanic",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        }
      ],
      "files_changed": 1,
      "insertions": 9,
      "deletions": 0
    },
    {
      "hash": "7ace8a608c4aabaeffda62ec954476647a32389f",
      "short_hash": "7ace8a6",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786914787,
      "message": "Documentation - TECHNICAL.md et mindmap pour SPOR-443 a SPOR-457\n\nEntrees techniques et mise a jour de la mindmap (statut Done) pour les\ncinq lots implementes dans les commits precedents : Materiel\nusure/entretien (SPOR-449/450/451/452), correction denivele + tuile\nDescent (SPOR-455/456), icones Sensations par niveau + tendances\nFeeling/Motivation (SPOR-447/448), Planificateur de parcours -\ntraces a usage unique / point-a-point / fusion (SPOR-453/454/457), et\nObjectifs Yearly + Dashboard (SPOR-443/444/445/446).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Documentation - TECHNICAL.md et mindmap pour SPOR-443 a SPOR-457",
      "type": "docs",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 1060,
          "deletions": 0,
          "lines_changed": 1060
        }
      ],
      "files_changed": 2,
      "insertions": 1077,
      "deletions": 0
    },
    {
      "hash": "71d30213ac17987c6b3a25c3a2be19747021499d",
      "short_hash": "71d3021",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786914769,
      "message": "Objectifs Yearly - Rattrapage chiffre, etat Planned et etoile pleine + Dashboard \"by sport\" (SPOR-443/444/445/446)\n\nSur /yearly : chiffre le rattrapage necessaire sur les 14 prochains jours\npour rester dans les temps (addendum a la fleche de tendance SPOR-440),\nmarque \"Planned\" avec compte a rebours un objectif dont l'annee cible n'a\npas encore commence (au lieu d'un retard qui n'a pas de sens), et rend\npleine l'etoile \"epingle au Dashboard\" au lieu d'un simple contour.\n\nSur le Dashboard, le bloc \"This week, by sport\" passe a cote du resume au\nlieu de systematiquement en dessous.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Objectifs Yearly - Rattrapage chiffre, etat Planned et etoile pleine + Dashboard \"by sport\" (SPOR-44",
      "type": "other",
      "modified_files": [
        "docs/objectifs.md",
        "src/Domain/Training/YearlyRecentTrend.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Goals/YearlyController.php",
        "src/Http/Goals/YearlyObjectiveProgress.php",
        "templates/dashboard.php",
        "templates/yearly.php",
        "tests/Domain/YearlyRecentTrendTest.php"
      ],
      "modified_files_details": [
        {
          "path": "docs/objectifs.md",
          "insertions": 28,
          "deletions": 8,
          "lines_changed": 36
        },
        {
          "path": "src/Domain/Training/YearlyRecentTrend.php",
          "insertions": 31,
          "deletions": 4,
          "lines_changed": 35
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 21,
          "deletions": 1,
          "lines_changed": 22
        },
        {
          "path": "src/Http/Goals/YearlyController.php",
          "insertions": 41,
          "deletions": 4,
          "lines_changed": 45
        },
        {
          "path": "src/Http/Goals/YearlyObjectiveProgress.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 16,
          "deletions": 3,
          "lines_changed": 19
        },
        {
          "path": "templates/yearly.php",
          "insertions": 23,
          "deletions": 3,
          "lines_changed": 26
        },
        {
          "path": "tests/Domain/YearlyRecentTrendTest.php",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        }
      ],
      "files_changed": 8,
      "insertions": 216,
      "deletions": 23
    },
    {
      "hash": "90ae1f4f58f8661eacb1c7a3b5607471eeddf89f",
      "short_hash": "90ae1f4",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786914757,
      "message": "Routes - Planificateur : traces a usage unique, point-a-point et fusion des chevauchements (SPOR-453/454/457)\n\nLe planificateur recombinait uniquement les parcours reconnus comme\nrecurrents (empruntes au moins deux fois) ; il inclut desormais aussi les\ntraces GPS parcourues une seule fois. Ajoute un mode point-a-point\noptionnel (arrivee differente du depart, prend le pas sur Boucle/\nAller-retour) et fusionne automatiquement les portions de trace communes\na plusieurs parcours sur la carte, pour rester lisible avec un long\nhistorique (PolylineSpatialIndex, RouteArcGraphCache).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Planificateur : traces a usage unique, point-a-point et fusion des chevauchements (SPOR-453",
      "type": "other",
      "modified_files": [
        "db/migrations/090_add_route_arc_graph_cache.sql",
        "docs/routes.md",
        "src/Domain/Training/GeoProjection.php",
        "src/Domain/Training/PolylineSpatialIndex.php",
        "src/Domain/Training/RouteArcGraph.php",
        "src/Domain/Training/RouteCombinationSearch.php",
        "src/Http/Routes/RoutePlannerController.php",
        "src/Infra/Db/RouteArcGraphCache.php",
        "templates/route_planner.php",
        "tests/Domain/PolylineSpatialIndexTest.php",
        "tests/Domain/RouteArcGraphTest.php",
        "tests/Domain/RouteCombinationSearchTest.php"
      ],
      "modified_files_details": [
        {
          "path": "db/migrations/090_add_route_arc_graph_cache.sql",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "docs/routes.md",
          "insertions": 30,
          "deletions": 12,
          "lines_changed": 42
        },
        {
          "path": "src/Domain/Training/GeoProjection.php",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "src/Domain/Training/PolylineSpatialIndex.php",
          "insertions": 175,
          "deletions": 0,
          "lines_changed": 175
        },
        {
          "path": "src/Domain/Training/RouteArcGraph.php",
          "insertions": 308,
          "deletions": 74,
          "lines_changed": 382
        },
        {
          "path": "src/Domain/Training/RouteCombinationSearch.php",
          "insertions": 163,
          "deletions": 30,
          "lines_changed": 193
        },
        {
          "path": "src/Http/Routes/RoutePlannerController.php",
          "insertions": 145,
          "deletions": 20,
          "lines_changed": 165
        },
        {
          "path": "src/Infra/Db/RouteArcGraphCache.php",
          "insertions": 121,
          "deletions": 0,
          "lines_changed": 121
        },
        {
          "path": "templates/route_planner.php",
          "insertions": 74,
          "deletions": 12,
          "lines_changed": 86
        },
        {
          "path": "tests/Domain/PolylineSpatialIndexTest.php",
          "insertions": 152,
          "deletions": 0,
          "lines_changed": 152
        },
        {
          "path": "tests/Domain/RouteArcGraphTest.php",
          "insertions": 205,
          "deletions": 6,
          "lines_changed": 211
        },
        {
          "path": "tests/Domain/RouteCombinationSearchTest.php",
          "insertions": 93,
          "deletions": 0,
          "lines_changed": 93
        }
      ],
      "files_changed": 12,
      "insertions": 1522,
      "deletions": 154
    },
    {
      "hash": "18187a2be517ab172c7a60c96d4f3492bfa9afaa",
      "short_hash": "18187a2",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786914744,
      "message": "Sensations - Icones par niveau + tendances Feeling/Motivation sur Physiology (SPOR-447/448)\n\nLes 5 chips de notation Feeling/Motivation/Pain de l'onglet Sensations\netaient toutes rendues avec la meme icone generique, sans progression\nlisible avant de cliquer (retour utilisateur). Chaque niveau a desormais\nsa propre icone (visage qui sourit, eclair et flamme qui grandissent) et\nun remplissage progressif type notation etoiles (data-rating-group,\ndistinct des groupes de chips a selection unique).\n\nAjoute deux cartes de tendance Feeling/Motivation en tete de page\nPhysiology, basees sur la moyenne des sensations saisies par seance\n(SensationTrend, SessionSensationsRepository) selon le meme principe que\nles mesures manuelles existantes.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Sensations - Icones par niveau + tendances Feeling/Motivation sur Physiology (SPOR-447/448)",
      "type": "other",
      "modified_files": [
        "docs/sante.md",
        "public/assets/js/icons.js",
        "public/assets/js/interactions.js",
        "src/Domain/Training/SensationTrend.php",
        "src/Domain/Training/SessionSensations.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/SessionSensationsRepository.php",
        "templates/health.php",
        "templates/session_detail.php",
        "tests/Domain/SensationTrendTest.php",
        "tests/Domain/SessionSensationsTest.php"
      ],
      "modified_files_details": [
        {
          "path": "docs/sante.md",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "public/assets/js/icons.js",
          "insertions": 33,
          "deletions": 3,
          "lines_changed": 36
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 66,
          "deletions": 0,
          "lines_changed": 66
        },
        {
          "path": "src/Domain/Training/SensationTrend.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "src/Domain/Training/SessionSensations.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 91,
          "deletions": 0,
          "lines_changed": 91
        },
        {
          "path": "src/Http/Health/SessionSensationsRepository.php",
          "insertions": 73,
          "deletions": 0,
          "lines_changed": 73
        },
        {
          "path": "templates/health.php",
          "insertions": 62,
          "deletions": 0,
          "lines_changed": 62
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 57,
          "deletions": 27,
          "lines_changed": 84
        },
        {
          "path": "tests/Domain/SensationTrendTest.php",
          "insertions": 48,
          "deletions": 0,
          "lines_changed": 48
        },
        {
          "path": "tests/Domain/SessionSensationsTest.php",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        }
      ],
      "files_changed": 11,
      "insertions": 564,
      "deletions": 30
    },
    {
      "hash": "1b35756edb58ea51a4d951ebe18947c9e29f47b9",
      "short_hash": "1b35756",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786914731,
      "message": "Sessions - Correction denivele + altitude min/max et descente sur la tuile Elevation (SPOR-455/456)\n\nFiltre de bruit GPS/baro (micro-variations <3m) sur le denivele positif\ncumule, qui etait surestime par un cumul brut. Ajoute l'altitude min/max\net le denivele negatif (tuile \"Descent\") a cote de la tuile Elevation du\ndetail de seance, meme filtre de bruit applique. Reinitialise le\ndenivele des seances existantes (migration 088) pour recalcul au prochain\naffichage (session_elevation_checked_at, migration 089).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Sessions - Correction denivele + altitude min/max et descente sur la tuile Elevation (SPOR-455/456)",
      "type": "fix",
      "modified_files": [
        "db/migrations/088_reset_session_elevation_gain_for_noise_fix.sql",
        "db/migrations/089_add_session_elevation_checked_at.sql",
        "docs/sessions.md",
        "src/Domain/Import/ElevationGain.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionPresenter.php",
        "src/Infra/Db/ElevationBackfill.php",
        "tests/Domain/ElevationGainTest.php",
        "tests/Http/Sessions/SessionPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "db/migrations/088_reset_session_elevation_gain_for_noise_fix.sql",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "db/migrations/089_add_session_elevation_checked_at.sql",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "docs/sessions.md",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Domain/Import/ElevationGain.php",
          "insertions": 62,
          "deletions": 9,
          "lines_changed": 71
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 44,
          "deletions": 1,
          "lines_changed": 45
        },
        {
          "path": "src/Infra/Db/ElevationBackfill.php",
          "insertions": 29,
          "deletions": 8,
          "lines_changed": 37
        },
        {
          "path": "tests/Domain/ElevationGainTest.php",
          "insertions": 56,
          "deletions": 4,
          "lines_changed": 60
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        }
      ],
      "files_changed": 9,
      "insertions": 264,
      "deletions": 22
    },
    {
      "hash": "ef3f325a18fa7c4c48bf3bfe7dcd80d61cb5cedf",
      "short_hash": "ef3f325",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786914718,
      "message": "Materiel - Alerte d'usure configurable, entretien par composant et conseils par defaut (SPOR-449/450/451/452)\n\nSeuil d'alerte d'usure personnalisable par materiel (warn_percent, 85%\npar defaut), journal d'entretien libre avec suivi d'usure par composant\nremplacable (odometre a l'installation), et conseil d'entretien par\ndefaut suggere selon le sport dans le formulaire d'ajout/edition.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Materiel - Alerte d'usure configurable, entretien par composant et conseils par defaut (SPOR-449/450",
      "type": "other",
      "modified_files": [
        "config/routes.php",
        "db/migrations/086_add_gear_warn_percent.sql",
        "db/migrations/087_add_gear_service_entries.sql",
        "docs/materiel.md",
        "src/Domain/Training/GearCareTips.php",
        "src/Domain/Training/GearComponentWear.php",
        "src/Domain/Training/GearInput.php",
        "src/Domain/Training/GearServiceEntryInput.php",
        "src/Domain/Training/GearUsage.php",
        "src/Http/Gear/GearController.php",
        "src/Http/Gear/GearServiceLogController.php",
        "src/Http/Notifications/NotificationGenerator.php",
        "templates/gear.php",
        "templates/gear_detail.php",
        "templates/partials/gear_form_fields.php",
        "templates/partials/gear_service_log_modal.php",
        "tests/Domain/GearCareTipsTest.php",
        "tests/Domain/GearComponentWearTest.php",
        "tests/Domain/GearInputTest.php",
        "tests/Domain/GearServiceEntryInputTest.php",
        "tests/Domain/GearUsageTest.php"
      ],
      "modified_files_details": [
        {
          "path": "config/routes.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "db/migrations/086_add_gear_warn_percent.sql",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "db/migrations/087_add_gear_service_entries.sql",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "docs/materiel.md",
          "insertions": 35,
          "deletions": 1,
          "lines_changed": 36
        },
        {
          "path": "src/Domain/Training/GearCareTips.php",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "src/Domain/Training/GearComponentWear.php",
          "insertions": 68,
          "deletions": 0,
          "lines_changed": 68
        },
        {
          "path": "src/Domain/Training/GearInput.php",
          "insertions": 43,
          "deletions": 1,
          "lines_changed": 44
        },
        {
          "path": "src/Domain/Training/GearServiceEntryInput.php",
          "insertions": 140,
          "deletions": 0,
          "lines_changed": 140
        },
        {
          "path": "src/Domain/Training/GearUsage.php",
          "insertions": 22,
          "deletions": 9,
          "lines_changed": 31
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 103,
          "deletions": 9,
          "lines_changed": 112
        },
        {
          "path": "src/Http/Gear/GearServiceLogController.php",
          "insertions": 90,
          "deletions": 0,
          "lines_changed": 90
        },
        {
          "path": "src/Http/Notifications/NotificationGenerator.php",
          "insertions": 12,
          "deletions": 8,
          "lines_changed": 20
        },
        {
          "path": "templates/gear.php",
          "insertions": 30,
          "deletions": 3,
          "lines_changed": 33
        },
        {
          "path": "templates/gear_detail.php",
          "insertions": 24,
          "deletions": 2,
          "lines_changed": 26
        },
        {
          "path": "templates/partials/gear_form_fields.php",
          "insertions": 28,
          "deletions": 3,
          "lines_changed": 31
        },
        {
          "path": "templates/partials/gear_service_log_modal.php",
          "insertions": 90,
          "deletions": 0,
          "lines_changed": 90
        },
        {
          "path": "tests/Domain/GearCareTipsTest.php",
          "insertions": 42,
          "deletions": 0,
          "lines_changed": 42
        },
        {
          "path": "tests/Domain/GearComponentWearTest.php",
          "insertions": 111,
          "deletions": 0,
          "lines_changed": 111
        },
        {
          "path": "tests/Domain/GearInputTest.php",
          "insertions": 57,
          "deletions": 0,
          "lines_changed": 57
        },
        {
          "path": "tests/Domain/GearServiceEntryInputTest.php",
          "insertions": 194,
          "deletions": 0,
          "lines_changed": 194
        },
        {
          "path": "tests/Domain/GearUsageTest.php",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        }
      ],
      "files_changed": 21,
      "insertions": 1190,
      "deletions": 36
    },
    {
      "hash": "cbe83f9e6d3e23f216700df31033d5044f81345c",
      "short_hash": "cbe83f9",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786815675,
      "message": "Routes - Planificateur : melanger les traces Run pour Bike (SPOR-442)\n\nSymetrique de SPOR-438 (Bike->Run) : reglage Settings + override ponctuel\nindependant, decoche par defaut (une trace de course n'est pas forcement\npraticable a velo, contrairement au sens inverse). RoutePlannerController\ngeneralise de la paire figee Run-uniquement vers une paire bidirectionnelle\n(MIXABLE_PAIRS) ; RouteArcGraph/RouteCombinationSearch inchanges.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Planificateur : melanger les traces Run pour Bike (SPOR-442)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/085_add_mix_run_into_bike_planner.sql",
        "docs/routes.md",
        "src/Http/Routes/RoutePlannerController.php",
        "src/Http/Settings/SettingsController.php",
        "templates/route_planner.php",
        "templates/settings.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": "db/migrations/085_add_mix_run_into_bike_planner.sql",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "docs/routes.md",
          "insertions": 11,
          "deletions": 7,
          "lines_changed": 18
        },
        {
          "path": "src/Http/Routes/RoutePlannerController.php",
          "insertions": 35,
          "deletions": 26,
          "lines_changed": 61
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 8,
          "deletions": 2,
          "lines_changed": 10
        },
        {
          "path": "templates/route_planner.php",
          "insertions": 11,
          "deletions": 10,
          "lines_changed": 21
        },
        {
          "path": "templates/settings.php",
          "insertions": 5,
          "deletions": 2,
          "lines_changed": 7
        }
      ],
      "files_changed": 8,
      "insertions": 129,
      "deletions": 47
    },
    {
      "hash": "1a236a265a4898413fd2403f6cbfaeccb3dc529a",
      "short_hash": "1a236a2",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786814987,
      "message": "Objectifs Yearly - Dupliquer un objectif vers une autre annee (SPOR-441)\n\nBouton \"Duplicate...\" par carte (active ou terminee) : copie sport/\nsous-sport/label/cible vers une nouvelle annee cible (pre-remplie sur\nannee+1, modifiable), jamais l'epinglage Dashboard ni la progression.\nPratique en fin d'annee pour reconduire ses objectifs habituels.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Objectifs Yearly - Dupliquer un objectif vers une autre annee (SPOR-441)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/objectifs.md",
        "src/Http/Goals/YearlyController.php",
        "templates/partials/yearly_duplicate_modal.php",
        "templates/yearly.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 47,
          "deletions": 0,
          "lines_changed": 47
        },
        {
          "path": "config/routes.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "src/Http/Goals/YearlyController.php",
          "insertions": 77,
          "deletions": 0,
          "lines_changed": 77
        },
        {
          "path": "templates/partials/yearly_duplicate_modal.php",
          "insertions": 42,
          "deletions": 0,
          "lines_changed": 42
        },
        {
          "path": "templates/yearly.php",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        }
      ],
      "files_changed": 7,
      "insertions": 182,
      "deletions": 2
    },
    {
      "hash": "b8c71e974e4ef338f93d7f59adecb721c74984c8",
      "short_hash": "b8c71e9",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786814598,
      "message": "Objectifs Yearly - Icone de tendance sur les 14 derniers jours (SPOR-440)\n\nNouvel indicateur distinct du pace cumule existant (SPOR-434) : compare\nle volume ajoute sur les 14 derniers jours au rythme requis a partir de\nmaintenant pour atteindre la cible en fin d'annee. Affiche sur /yearly\n(nouvelle ligne) et le widget epingle du Dashboard (icone compacte).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Objectifs Yearly - Icone de tendance sur les 14 derniers jours (SPOR-440)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/objectifs.md",
        "src/Domain/Training/YearlyRecentTrend.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Goals/YearlyController.php",
        "src/Http/Goals/YearlyObjectiveProgress.php",
        "templates/dashboard.php",
        "templates/yearly.php",
        "tests/Domain/YearlyRecentTrendTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 57,
          "deletions": 0,
          "lines_changed": 57
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "src/Domain/Training/YearlyRecentTrend.php",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 17,
          "deletions": 8,
          "lines_changed": 25
        },
        {
          "path": "src/Http/Goals/YearlyController.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "src/Http/Goals/YearlyObjectiveProgress.php",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "templates/yearly.php",
          "insertions": 6,
          "deletions": 1,
          "lines_changed": 7
        },
        {
          "path": "tests/Domain/YearlyRecentTrendTest.php",
          "insertions": 82,
          "deletions": 0,
          "lines_changed": 82
        }
      ],
      "files_changed": 10,
      "insertions": 266,
      "deletions": 10
    },
    {
      "hash": "6faade9da02cd750ac03fdb0a392b8ea17f27f61",
      "short_hash": "6faade9",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786813908,
      "message": "Routes - Export GPX d'un parcours pour import Garmin (SPOR-439)\n\nBouton \"Export GPX\" sur la fiche detail d'un parcours : telecharge un\nfichier .gpx 1.1 standard (construit depuis l'empreinte deja stockee,\nDOMDocument pour un echappement XML correct) que l'utilisateur importe\nlui-meme dans Garmin Connect/Garmin Express - aucune integration OAuth,\naucun compte tiers connecte.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Export GPX d'un parcours pour import Garmin (SPOR-439)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/routes.md",
        "src/Domain/Training/GpxExport.php",
        "src/Http/Routes/RoutesController.php",
        "templates/route_detail.php",
        "tests/Domain/GpxExportTest.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": "config/routes.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "docs/routes.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "src/Domain/Training/GpxExport.php",
          "insertions": 82,
          "deletions": 0,
          "lines_changed": 82
        },
        {
          "path": "src/Http/Routes/RoutesController.php",
          "insertions": 43,
          "deletions": 0,
          "lines_changed": 43
        },
        {
          "path": "templates/route_detail.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "tests/Domain/GpxExportTest.php",
          "insertions": 91,
          "deletions": 0,
          "lines_changed": 91
        }
      ],
      "files_changed": 8,
      "insertions": 270,
      "deletions": 0
    },
    {
      "hash": "215195df5147914baa6858840d83702204cf83fc",
      "short_hash": "215195d",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786813536,
      "message": "Routes - Planificateur : melanger les traces Bike pour Run (SPOR-438)\n\nUne trace roulee peut servir de base a un parcours de course : nouveau\nreglage Settings > Analysis (persistant, defaut ON) + override ponctuel\nsur /routes/plan pour inclure les parcours Bike dans la recherche de\ncombinaison quand le sport est Run. Toujours asymetrique (Bike reste\nBike-only). RouteArcGraph/RouteCombinationSearch inchanges - melanger\nn'est qu'une question de quelles routes fetchRoutesForPlanner() recupere.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Planificateur : melanger les traces Bike pour Run (SPOR-438)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/084_add_mix_bike_into_run_planner.sql",
        "docs/routes.md",
        "src/Http/Routes/RoutePlannerController.php",
        "src/Http/Settings/SettingsController.php",
        "templates/route_planner.php",
        "templates/settings.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 56,
          "deletions": 1,
          "lines_changed": 57
        },
        {
          "path": "db/migrations/084_add_mix_bike_into_run_planner.sql",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "docs/routes.md",
          "insertions": 7,
          "deletions": 1,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Routes/RoutePlannerController.php",
          "insertions": 51,
          "deletions": 12,
          "lines_changed": 63
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 9,
          "deletions": 2,
          "lines_changed": 11
        },
        {
          "path": "templates/route_planner.php",
          "insertions": 42,
          "deletions": 5,
          "lines_changed": 47
        },
        {
          "path": "templates/settings.php",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        }
      ],
      "files_changed": 8,
      "insertions": 186,
      "deletions": 22
    },
    {
      "hash": "082d1d7bf25a174bddc91d318e3efc0fd1035876",
      "short_hash": "082d1d7",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786812462,
      "message": "Routes - Planificateur : combiner des parcours existants (SPOR-437)\n\nRepond au blocage de faisabilite de SPOR-411/418 (pas de moteur de\nrouting sur graphe routier reel sous contrainte FTP-only) en recombinant\nles parcours DEJA enregistres de l'utilisateur : detection de jonctions\nentre traces (RouteArcGraph), decoupage en arcs, recherche bornee de\ncombinaisons sous contrainte de distance (RouteCombinationSearch, boucle\nou aller-retour), scoring d'appetit denivele. Nouvelle page /routes/plan\n(carte cliquable, jusqu'a 3 propositions, sauvegarde comme parcours\n\"planned\"). Maquette mise a jour (route-plan.html remplace le flux\nadresse libre de routes.html, devenu technique inexact).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Planificateur : combiner des parcours existants (SPOR-437)",
      "type": "other",
      "modified_files": [
        "References/design/app/route-plan.html",
        "References/design/app/routes.html",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/083_add_route_source.sql",
        "docs/routes.md",
        "src/Domain/Training/ElevationAppetite.php",
        "src/Domain/Training/GeoProjection.php",
        "src/Domain/Training/RouteArcGraph.php",
        "src/Domain/Training/RouteCombinationSearch.php",
        "src/Domain/Training/RouteFingerprint.php",
        "src/Domain/Training/RouteSegmentNovelty.php",
        "src/Http/Routes/GpsSportCodes.php",
        "src/Http/Routes/RoutePlannerController.php",
        "templates/route_planner.php",
        "templates/routes.php",
        "tests/Domain/ElevationAppetiteTest.php",
        "tests/Domain/GeoProjectionTest.php",
        "tests/Domain/RouteArcGraphTest.php",
        "tests/Domain/RouteCombinationSearchTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/app/route-plan.html",
          "insertions": 158,
          "deletions": 0,
          "lines_changed": 158
        },
        {
          "path": "References/design/app/routes.html",
          "insertions": 1,
          "deletions": 73,
          "lines_changed": 74
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 11,
          "deletions": 8,
          "lines_changed": 19
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 203,
          "deletions": 0,
          "lines_changed": 203
        },
        {
          "path": "config/routes.php",
          "insertions": 15,
          "deletions": 2,
          "lines_changed": 17
        },
        {
          "path": "db/migrations/083_add_route_source.sql",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "docs/routes.md",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "src/Domain/Training/ElevationAppetite.php",
          "insertions": 101,
          "deletions": 0,
          "lines_changed": 101
        },
        {
          "path": "src/Domain/Training/GeoProjection.php",
          "insertions": 107,
          "deletions": 0,
          "lines_changed": 107
        },
        {
          "path": "src/Domain/Training/RouteArcGraph.php",
          "insertions": 404,
          "deletions": 0,
          "lines_changed": 404
        },
        {
          "path": "src/Domain/Training/RouteCombinationSearch.php",
          "insertions": 294,
          "deletions": 0,
          "lines_changed": 294
        },
        {
          "path": "src/Domain/Training/RouteFingerprint.php",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "src/Domain/Training/RouteSegmentNovelty.php",
          "insertions": 6,
          "deletions": 61,
          "lines_changed": 67
        },
        {
          "path": "src/Http/Routes/GpsSportCodes.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "src/Http/Routes/RoutePlannerController.php",
          "insertions": 367,
          "deletions": 0,
          "lines_changed": 367
        },
        {
          "path": "templates/route_planner.php",
          "insertions": 286,
          "deletions": 0,
          "lines_changed": 286
        },
        {
          "path": "templates/routes.php",
          "insertions": 10,
          "deletions": 7,
          "lines_changed": 17
        },
        {
          "path": "tests/Domain/ElevationAppetiteTest.php",
          "insertions": 90,
          "deletions": 0,
          "lines_changed": 90
        },
        {
          "path": "tests/Domain/GeoProjectionTest.php",
          "insertions": 157,
          "deletions": 0,
          "lines_changed": 157
        },
        {
          "path": "tests/Domain/RouteArcGraphTest.php",
          "insertions": 237,
          "deletions": 0,
          "lines_changed": 237
        },
        {
          "path": "tests/Domain/RouteCombinationSearchTest.php",
          "insertions": 176,
          "deletions": 0,
          "lines_changed": 176
        }
      ],
      "files_changed": 21,
      "insertions": 2727,
      "deletions": 151
    },
    {
      "hash": "6bf2b6815e850d03aecb594f8903a434a0574e02",
      "short_hash": "6bf2b68",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786804423,
      "message": "Routes - Etude de faisabilite du planificateur de parcours (SPOR-418)\n\nConfirme le Won't de SPOR-411 : routing/geocodage tiers viole la vie\nprivee non negociable du projet, alternative 100% self-hosted (extrait\nOSM + pathfinding PHP + donnees d'altitude) disproportionnee sous\ncontrainte FTP-only. Detail dans TECHNICAL.md.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Etude de faisabilite du planificateur de parcours (SPOR-418)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 9,
          "deletions": 5,
          "lines_changed": 14
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 93,
          "deletions": 0,
          "lines_changed": 93
        }
      ],
      "files_changed": 2,
      "insertions": 102,
      "deletions": 5
    },
    {
      "hash": "1d704b7460c6d3c5a3594442ad840391bbb68604",
      "short_hash": "1d704b7",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786804107,
      "message": "Objectifs - Yearly : nouvel objectif annuel, tri, tendance et Dashboard\n\n- SPOR-427/428/429 : type d'objectif Yearly (cible annuelle par sport/\n  sous-sport, distance ou duree), epinglage sur le Dashboard (5 slots max),\n  regroupement sidebar Goals/Races/Yearly sous \"Objectives\".\n- SPOR-431 : tri par glisser-deposer des objectifs actifs (/yearly et\n  Dashboard, meme ordre partage).\n- SPOR-433/436 : fusion Year progress + Yearly objectives en une seule\n  carte Dashboard, barres alignees sur une colonne label a largeur fixe.\n- SPOR-434/435 : indicateur avance/retard vs un rythme lineaire depuis le\n  1er janvier, avec repere visuel sur la barre de /yearly.\n- SPOR-430/432 (issus d'une autre session) : navigation Previous/Next sur\n  le detail de seance, regroupement sidebar Physiology/Injuries sous\n  \"Health\".\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Objectifs - Yearly : nouvel objectif annuel, tri, tendance et Dashboard",
      "type": "other",
      "modified_files": [
        "References/design/app/session-detail.html",
        "References/design/assets/css/app.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/081_add_goal_yearly_type.sql",
        "db/migrations/082_add_goal_display_order.sql",
        "docs/dashboard.md",
        "docs/objectifs.md",
        "docs/pain-injuries.md",
        "docs/sante.md",
        "docs/sessions.md",
        "public/assets/css/app.css",
        "public/assets/js/interactions.js",
        "src/Domain/Training/GoalPeriod.php",
        "src/Domain/Training/YearlyPaceGap.php",
        "src/Http/Admin/AdminBadgesController.php",
        "src/Http/Admin/AdminChecklistController.php",
        "src/Http/Admin/AdminQuotesController.php",
        "src/Http/Admin/AdminSessionTagsController.php",
        "src/Http/Admin/AdminSportsController.php",
        "src/Http/Admin/InvitationsController.php",
        "src/Http/Admin/LogsController.php",
        "src/Http/Calendar/CalendarController.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Gear/GearAutoAssignController.php",
        "src/Http/Gear/GearController.php",
        "src/Http/Goals/GoalFormHelpers.php",
        "src/Http/Goals/GoalsController.php",
        "src/Http/Goals/RacesController.php",
        "src/Http/Goals/YearlyController.php",
        "src/Http/Goals/YearlyObjectiveProgress.php",
        "src/Http/Goals/YearlySidebarCounter.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/InjurySidebarCounter.php",
        "src/Http/Health/PainInjuriesController.php",
        "src/Http/Notifications/NotificationsController.php",
        "src/Http/Profile/ProfileController.php",
        "src/Http/Records/BadgesController.php",
        "src/Http/Records/RecordsController.php",
        "src/Http/Routes/RoutesController.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionNavigation.php",
        "src/Http/Sessions/SessionPresenter.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Http/Settings/SettingsController.php",
        "src/Http/Trends/TrendsController.php",
        "templates/TODO.md",
        "templates/dashboard.php",
        "templates/health.php",
        "templates/pain_injuries.php",
        "templates/partials/app_sidebar.php",
        "templates/partials/goal_edit_modal.php",
        "templates/partials/goal_form_fields.php",
        "templates/session_detail.php",
        "templates/yearly.php",
        "tests/Domain/GoalPeriodTest.php",
        "tests/Domain/YearlyPaceGapTest.php",
        "tests/Http/Sessions/SessionPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/app/session-detail.html",
          "insertions": 7,
          "deletions": 1,
          "lines_changed": 8
        },
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 556,
          "deletions": 0,
          "lines_changed": 556
        },
        {
          "path": "config/routes.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "db/migrations/081_add_goal_yearly_type.sql",
          "insertions": 69,
          "deletions": 0,
          "lines_changed": 69
        },
        {
          "path": "db/migrations/082_add_goal_display_order.sql",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "docs/dashboard.md",
          "insertions": 26,
          "deletions": 3,
          "lines_changed": 29
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 78,
          "deletions": 7,
          "lines_changed": 85
        },
        {
          "path": "docs/pain-injuries.md",
          "insertions": 7,
          "deletions": 7,
          "lines_changed": 14
        },
        {
          "path": "docs/sante.md",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "docs/sessions.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 69,
          "deletions": 1,
          "lines_changed": 70
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 67,
          "deletions": 0,
          "lines_changed": 67
        },
        {
          "path": "src/Domain/Training/GoalPeriod.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "src/Domain/Training/YearlyPaceGap.php",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "src/Http/Admin/AdminBadgesController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/AdminChecklistController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/AdminQuotesController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/AdminSessionTagsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/AdminSportsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/InvitationsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/LogsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Calendar/CalendarController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 59,
          "deletions": 0,
          "lines_changed": 59
        },
        {
          "path": "src/Http/Gear/GearAutoAssignController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Http/Goals/GoalFormHelpers.php",
          "insertions": 49,
          "deletions": 8,
          "lines_changed": 57
        },
        {
          "path": "src/Http/Goals/GoalsController.php",
          "insertions": 1,
          "deletions": 24,
          "lines_changed": 25
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Http/Goals/YearlyController.php",
          "insertions": 504,
          "deletions": 0,
          "lines_changed": 504
        },
        {
          "path": "src/Http/Goals/YearlyObjectiveProgress.php",
          "insertions": 86,
          "deletions": 0,
          "lines_changed": 86
        },
        {
          "path": "src/Http/Goals/YearlySidebarCounter.php",
          "insertions": 22,
          "deletions": 0,
          "lines_changed": 22
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Health/InjurySidebarCounter.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Health/PainInjuriesController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Notifications/NotificationsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Profile/ProfileController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Records/BadgesController.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Http/Records/RecordsController.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Http/Routes/RoutesController.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 11,
          "deletions": 1,
          "lines_changed": 12
        },
        {
          "path": "src/Http/Sessions/SessionNavigation.php",
          "insertions": 48,
          "deletions": 0,
          "lines_changed": 48
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 12,
          "deletions": 1,
          "lines_changed": 13
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "templates/TODO.md",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 57,
          "deletions": 7,
          "lines_changed": 64
        },
        {
          "path": "templates/health.php",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "templates/pain_injuries.php",
          "insertions": 3,
          "deletions": 3,
          "lines_changed": 6
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 39,
          "deletions": 24,
          "lines_changed": 63
        },
        {
          "path": "templates/partials/goal_edit_modal.php",
          "insertions": 17,
          "deletions": 5,
          "lines_changed": 22
        },
        {
          "path": "templates/partials/goal_form_fields.php",
          "insertions": 71,
          "deletions": 4,
          "lines_changed": 75
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 14,
          "deletions": 1,
          "lines_changed": 15
        },
        {
          "path": "templates/yearly.php",
          "insertions": 218,
          "deletions": 0,
          "lines_changed": 218
        },
        {
          "path": "tests/Domain/GoalPeriodTest.php",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "tests/Domain/YearlyPaceGapTest.php",
          "insertions": 69,
          "deletions": 0,
          "lines_changed": 69
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        }
      ],
      "files_changed": 59,
      "insertions": 2343,
      "deletions": 102
    },
    {
      "hash": "9ee6181f4e24bf7c4e3affd33c2939665d45a708",
      "short_hash": "9ee6181",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786790636,
      "message": "Routes - Ne comparer Known/New qu'aux seances anterieures du parcours\n\nLa seance fondatrice d'un parcours (la toute premiere fois qu'un trace est\nparcouru) se retrouvait a tort marquee \"Known\" au lieu de \"New\" : le\ndefaut bon marche la comparait a l'empreinte du parcours, qui est une\ncopie figee de... cette meme seance fondatrice (se comparer a soi-meme).\n\"Precise comparison\" comparait a TOUTES les autres seances du parcours,\ny compris des occurrences posterieures - juger une seance passee avec des\ndonnees qui n'existaient pas encore a l'epoque.\n\nLes deux comparaisons ne considerent desormais que les seances started_at\n< celle affichee. Le defaut bon marche n'utilise plus l'empreinte figee du\nparcours mais l'union des empreintes deja stockees des seances anterieures\n(toujours aucun reparse XML, et plus robuste qu'une seule trace de\nreference).\n\nSPOR-426. Verifie en reel sur les 4 occurrences reelles de route 23 :\nla fondatrice passe de 0% a 100% new, les occurrences suivantes restent\ncoherentes (2e = petite variation reelle, 3e/4e = deja bien etabli).\nSuite complete verte : 926 tests, 2787 assertions.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Ne comparer Known/New qu'aux seances anterieures du parcours",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "src/Http/Sessions/RouteNoveltyController.php",
        "src/Http/Sessions/SessionDetailController.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 69,
          "deletions": 0,
          "lines_changed": 69
        },
        {
          "path": "src/Http/Sessions/RouteNoveltyController.php",
          "insertions": 33,
          "deletions": 16,
          "lines_changed": 49
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 47,
          "deletions": 27,
          "lines_changed": 74
        }
      ],
      "files_changed": 4,
      "insertions": 150,
      "deletions": 43
    },
    {
      "hash": "1eca3b9072d40bc4981ef839214e11b06365eba2",
      "short_hash": "1eca3b9",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786787524,
      "message": "Routes - Empreinte de parcours densifiee a 150 echantillons (etait 40)\n\nSuite au fix distance point-segment (SPOR-424) : RouteFingerprint::SAMPLE_COUNT\npasse de 40 a 150, ce qui reduit encore l'erreur residuelle de \"corde vs\narc\" dans les virages (moins d'ecart entre deux echantillons consecutifs).\n\nNouvelle classe Infra\\Db\\RouteFingerprintBackfill (bin/public entry points,\nmeme paire que RouteBackfill) : regenere les empreintes deja presentes mais\nconstruites avec un SAMPLE_COUNT plus ancien (cible via JSON_LENGTH(...) <\nSAMPLE_COUNT courant - rejouable a l'identique si la constante est\naugmentee de nouveau), puis resynchronise routes.polyline_fingerprint\n(copie figee de la seance fondatrice, jamais mise a jour automatiquement).\n\nBug attrape et corrige en cours de route : la premiere version chargeait\ntcx_content pour tout un lot de 20 en un seul fetchAll(), exactement\nl'anti-pattern documente dans RouteBackfill/ElevationBackfill - fait\nplanter le premier essai (OOM). Corrige en repassant sur le motif etabli\n(id seuls, puis un blob a la fois dans la boucle).\n\nExecute en reel jusqu'a convergence : 157 seances regenerees, tous les\nparcours resynchronises. Sur la seance ayant motive SPOR-424 (id 165) :\nestimation par defaut 1.2km -> 0.5km, comparaison precise inchangee (0km).\n\nSPOR-425. Suite complete verte : 926 tests, 2787 assertions.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Empreinte de parcours densifiee a 150 echantillons (etait 40)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "bin/backfill_route_fingerprints.php",
        "public/backfill_route_fingerprints.php",
        "src/Domain/Training/RouteFingerprint.php",
        "src/Infra/Db/RouteFingerprintBackfill.php",
        "tests/Domain/RouteFingerprintTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 62,
          "deletions": 0,
          "lines_changed": 62
        },
        {
          "path": "bin/backfill_route_fingerprints.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "public/backfill_route_fingerprints.php",
          "insertions": 96,
          "deletions": 0,
          "lines_changed": 96
        },
        {
          "path": "src/Domain/Training/RouteFingerprint.php",
          "insertions": 23,
          "deletions": 11,
          "lines_changed": 34
        },
        {
          "path": "src/Infra/Db/RouteFingerprintBackfill.php",
          "insertions": 151,
          "deletions": 0,
          "lines_changed": 151
        },
        {
          "path": "tests/Domain/RouteFingerprintTest.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        }
      ],
      "files_changed": 7,
      "insertions": 374,
      "deletions": 12
    },
    {
      "hash": "295f4753e1da30f0fc89c510ad61708109dbc9ae",
      "short_hash": "295f475",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786785753,
      "message": "Routes - Distance point-segment au lieu de point-point pour Known/New\n\nL'estimation \"km nouveaux\" surestimait largement : l'empreinte de parcours\n(40 echantillons) n'a qu'un point tous les ~229m en moyenne, largement\nau-dela de la tolerance de detection (45m) - un point de la trace courante\nau milieu de deux echantillons consecutifs (donc ~111m de chacun) etait a\ntort marque \"inedit\" alors qu'il est geometriquement sur le parcours\nconnu, mesure au plus proche voisin.\n\nRouteSegmentNovelty mesure desormais une distance point -> SEGMENT\n(projection perpendiculaire sur chaque troncon d'une polyligne de\nreference ordonnee) au lieu de point -> point le plus proche.\nflagNewPoints() prend une LISTE DE POLYLIGNES plutot qu'une liste plate de\npoints, pour ne jamais relier par erreur deux traces sans rapport\n(RouteNoveltyController::referencePolylines(), une polyligne par seance\nliee au lieu d'une concatenation plate).\n\nVerifie en reel sur la seance qui a motive le signalement : 6.3km -> 1.2km\npar defaut, \"Precise comparison\" confirme 0km (inchange, deja fiable).\n\nSPOR-424. Tests dedies (point a mi-chemin entre deux echantillons, point\nhors segment toujours detecte, regression anti-troncon-fictif). Suite\ncomplete verte : 926 tests, 2567 assertions.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Distance point-segment au lieu de point-point pour Known/New",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "src/Domain/Training/RouteSegmentNovelty.php",
        "src/Http/Sessions/RouteNoveltyController.php",
        "src/Http/Sessions/SessionDetailController.php",
        "tests/Domain/RouteSegmentNoveltyTest.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": "src/Domain/Training/RouteSegmentNovelty.php",
          "insertions": 95,
          "deletions": 19,
          "lines_changed": 114
        },
        {
          "path": "src/Http/Sessions/RouteNoveltyController.php",
          "insertions": 20,
          "deletions": 13,
          "lines_changed": 33
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 9,
          "deletions": 2,
          "lines_changed": 11
        },
        {
          "path": "tests/Domain/RouteSegmentNoveltyTest.php",
          "insertions": 68,
          "deletions": 8,
          "lines_changed": 76
        }
      ],
      "files_changed": 6,
      "insertions": 265,
      "deletions": 42
    },
    {
      "hash": "93411bd87e6d918879e67332e0515e5aa5562b6b",
      "short_hash": "93411bd",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786783040,
      "message": "Routes - Couleurs fixes vert/rouge pour Known/New, trait plein, exception triathlon\n\nRevient sur l'approche precedente (pointilles) suite a un retour direct de\nl'utilisateur : au lieu de garder la couleur du sport pour \"Known\" et\ndistinguer \"New\" par un style de trait, les deux etats utilisent desormais\ndes couleurs FIXES et dediees (--route-known vert / --route-new rouge,\ntokens.css), independantes du sport, trait plein comme partout ailleurs\ndans l'app.\n\nException explicite pour les seances triathlon/multisport : la carte\ngarde sa couleur de sport habituelle, sans distinction Known/New (deja\ndecomposee par discipline dans l'onglet Segments) - data-novelty n'est\nsimplement jamais pose sur la carte pour ce cas, renderRoute() retombe\nsur son chemin existant a une seule couleur.\n\nSPOR-423 mis a jour en consequence. Verifie en reel (Playwright).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Couleurs fixes vert/rouge pour Known/New, trait plein, exception triathlon",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "docs/routes.md",
        "public/assets/css/tokens.css",
        "templates/session_detail.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 33,
          "deletions": 23,
          "lines_changed": 56
        },
        {
          "path": "docs/routes.md",
          "insertions": 10,
          "deletions": 9,
          "lines_changed": 19
        },
        {
          "path": "public/assets/css/tokens.css",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 38,
          "deletions": 25,
          "lines_changed": 63
        }
      ],
      "files_changed": 4,
      "insertions": 94,
      "deletions": 57
    },
    {
      "hash": "927204bdb5be8b5514c5ca6e250e274ca30cc44f",
      "short_hash": "927204b",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786782517,
      "message": "Routes - Distinction Known/New sur la carte par pointilles, pas seulement la couleur\n\nRetour utilisateur : le rouge/corail de la couleur \"Run\" et le rose de la\nportion \"New\" etaient trop proches pour etre distingues d'un coup d'oeil.\nLes couleurs de sport etant personnalisables, aucune teinte fixe ne peut\ngarantir un contraste suffisant dans tous les cas - la portion \"New\" est\ndesormais tracee en pointilles sur la carte (en plus de sa couleur), et la\nlegende reprend le meme code trait plein/pointille au lieu de deux\npastilles pleines.\n\nSPOR-423. Verifie en reel (Playwright, capture recadree sur la trace).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Distinction Known/New sur la carte par pointilles, pas seulement la couleur",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/routes.md",
        "templates/session_detail.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": "docs/routes.md",
          "insertions": 10,
          "deletions": 8,
          "lines_changed": 18
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 23,
          "deletions": 5,
          "lines_changed": 28
        }
      ],
      "files_changed": 4,
      "insertions": 74,
      "deletions": 13
    },
    {
      "hash": "247041285be2a6cf769a9633bdcfbc034774a65a",
      "short_hash": "2470412",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786751848,
      "message": "Routes - Tri de la liste + km inedits ; Pain & Injuries - 2 bugfix (badge, modales)\n\nTraite en autonomie (4 items de templates/TODO.md) :\n\n- Routes (SPOR-419) : tri de /routes par date/frequence/distance\n  (RouteSortOption, whitelist fermee, ?sort= server-side).\n- Routes (SPOR-420) : distance des segments inedits affichee en km a cote\n  de la carte bicolore du detail de seance (RouteSegmentNovelty::\n  noveltyDistanceMeters), mise a jour par \"Precise comparison\".\n- Pain & Injuries (SPOR-421, bug) : le badge de tendance (Improving/\n  Stable/Worsening) restait affiche indefiniment sur une blessure resolue\n  faute de condition sur le statut dans le template.\n- Pain & Injuries (SPOR-422, bug) : modales Log/Edit/Detail elargies\n  (rv-modal--lg) et reorganisees en 2 colonnes (rv-edit-grid, deja\n  definie en CSS mais jamais cablee) - silhouette a gauche, reste du\n  contenu a droite, plus de scroll interne.\n\nTests unitaires + doc utilisateur + Jira (Done) + mindmap a jour pour les\n4 items. Verifie en reel (Playwright, session forgee) pour les deux\nbugfix Pain & Injuries. Suite complete verte : 922 tests, 2563 assertions.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Tri de la liste + km inedits ; Pain & Injuries - 2 bugfix (badge, modales)",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/pain-injuries.md",
        "docs/routes.md",
        "src/Domain/Training/RouteSegmentNovelty.php",
        "src/Http/Routes/RouteSortOption.php",
        "src/Http/Routes/RoutesController.php",
        "src/Http/Sessions/RouteNoveltyController.php",
        "src/Http/Sessions/SessionDetailController.php",
        "templates/pain_injuries.php",
        "templates/partials/injury_form_fields.php",
        "templates/routes.php",
        "templates/session_detail.php",
        "tests/Domain/RouteSegmentNoveltyTest.php",
        "tests/Http/Routes/RouteSortOptionTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 183,
          "deletions": 0,
          "lines_changed": 183
        },
        {
          "path": "docs/pain-injuries.md",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "docs/routes.md",
          "insertions": 17,
          "deletions": 9,
          "lines_changed": 26
        },
        {
          "path": "src/Domain/Training/RouteSegmentNovelty.php",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "src/Http/Routes/RouteSortOption.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "src/Http/Routes/RoutesController.php",
          "insertions": 10,
          "deletions": 3,
          "lines_changed": 13
        },
        {
          "path": "src/Http/Sessions/RouteNoveltyController.php",
          "insertions": 11,
          "deletions": 1,
          "lines_changed": 12
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "templates/pain_injuries.php",
          "insertions": 35,
          "deletions": 5,
          "lines_changed": 40
        },
        {
          "path": "templates/partials/injury_form_fields.php",
          "insertions": 94,
          "deletions": 79,
          "lines_changed": 173
        },
        {
          "path": "templates/routes.php",
          "insertions": 33,
          "deletions": 8,
          "lines_changed": 41
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 8,
          "deletions": 1,
          "lines_changed": 9
        },
        {
          "path": "tests/Domain/RouteSegmentNoveltyTest.php",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "tests/Http/Routes/RouteSortOptionTest.php",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        }
      ],
      "files_changed": 15,
      "insertions": 549,
      "deletions": 107
    },
    {
      "hash": "d6547a616a532204bea19fe82463fc46b31a2953",
      "short_hash": "d6547a6",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786750375,
      "message": "Routes - Corrige la famine du backfill (bike/swim/run recents jamais atteints)\n\nRouteBackfill laissait polyline_fingerprint a NULL pour toute seance sans\nGPS exploitable - indiscernable de \"jamais tentee\", donc re-selectionnee a\nchaque appel (ORDER BY started_at ASC LIMIT 20). Des qu'il y a plus de 20\nvieilles seances sans GPS (natation piscine, PPG, yoga accumules depuis\n2013), aucune seance plus recente avec une vraie trace n'est jamais\natteinte. Marque desormais explicitement \"tente, pas de GPS\" (sentinel\nJSON 'null', deja gere comme non-match partout ou une empreinte est\ndecodee) plutot que de laisser NULL.\n\nExecute en reel : 8 seances bike/swim/run recentes enfin fingerprintees,\n2 nouveaux parcours Bike decouverts.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Corrige la famine du backfill (bike/swim/run recents jamais atteints)",
      "type": "other",
      "modified_files": [
        "TECHNICAL.md",
        "src/Infra/Db/RouteBackfill.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        },
        {
          "path": "src/Infra/Db/RouteBackfill.php",
          "insertions": 23,
          "deletions": 4,
          "lines_changed": 27
        }
      ],
      "files_changed": 2,
      "insertions": 76,
      "deletions": 4
    },
    {
      "hash": "3d2ab4a6a19bb3ca86ff1c7f39abac1079e074bb",
      "short_hash": "3d2ab4a",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786750010,
      "message": "Routes - Restreint le filtre aux sports GPS (Swim/Bike/Run/Golf/Marche/Triathlon/Rammeur/Ski alpin)\n\nListe curee et codee en dur (pas deduite des routes/sessions existantes -\ninclut des sports personnalises du catalogue de l'utilisateur qui n'auraient\npas de sens ailleurs). N'affecte que les chips du filtre, le reste de la\npage continue d'utiliser le catalogue complet (badges de sport, etc.).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Restreint le filtre aux sports GPS (Swim/Bike/Run/Golf/Marche/Triathlon/Rammeur/Ski alpin)",
      "type": "other",
      "modified_files": [
        "TECHNICAL.md",
        "docs/routes.md",
        "templates/routes.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 42,
          "deletions": 0,
          "lines_changed": 42
        },
        {
          "path": "docs/routes.md",
          "insertions": 6,
          "deletions": 3,
          "lines_changed": 9
        },
        {
          "path": "templates/routes.php",
          "insertions": 11,
          "deletions": 2,
          "lines_changed": 13
        }
      ],
      "files_changed": 3,
      "insertions": 59,
      "deletions": 5
    },
    {
      "hash": "d62dbacf91e6f6549a224056967fa2c0afe7e3a6",
      "short_hash": "d62dbac",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786749676,
      "message": "Routes - Carte cliquable, medailles or/argent/bronze sur les meilleurs temps\n\n- Toute la carte /routes navigue vers le detail (data-route-href + JS,\n  pas un <a> englobant - le bouton Rename... reste utilisable).\n- Corrige au passage la meme omission de leaflet.css que routes.php sur\n  route_detail.php (la grande carte aurait ete cassee identiquement).\n- Les 3 temps les plus rapides d'un parcours recoivent une medaille\n  (gold/silver/bronze), independamment de leur rang chronologique dans\n  l'historique - RoutesController::withMedals().\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Carte cliquable, medailles or/argent/bronze sur les meilleurs temps",
      "type": "other",
      "modified_files": [
        "TECHNICAL.md",
        "docs/routes.md",
        "src/Http/Routes/RoutesController.php",
        "templates/route_detail.php",
        "templates/routes.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 63,
          "deletions": 0,
          "lines_changed": 63
        },
        {
          "path": "docs/routes.md",
          "insertions": 16,
          "deletions": 8,
          "lines_changed": 24
        },
        {
          "path": "src/Http/Routes/RoutesController.php",
          "insertions": 36,
          "deletions": 2,
          "lines_changed": 38
        },
        {
          "path": "templates/route_detail.php",
          "insertions": 18,
          "deletions": 2,
          "lines_changed": 20
        },
        {
          "path": "templates/routes.php",
          "insertions": 17,
          "deletions": 2,
          "lines_changed": 19
        }
      ],
      "files_changed": 5,
      "insertions": 150,
      "deletions": 14
    },
    {
      "hash": "fa8e9380c93dea9fc51c10f3a2aea67a3656365e",
      "short_hash": "fa8e938",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786749187,
      "message": "Routes - Filtre par sport (comme /races)\n\nChips 100% client (data-filter-group, deja gere par interactions.js,\naucun JS nouveau) au-dessus de la liste de parcours - meme mecanisme\nque /races et /gear.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Filtre par sport (comme /races)",
      "type": "other",
      "modified_files": [
        "TECHNICAL.md",
        "docs/routes.md",
        "templates/routes.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "docs/routes.md",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "templates/routes.php",
          "insertions": 14,
          "deletions": 1,
          "lines_changed": 15
        }
      ],
      "files_changed": 3,
      "insertions": 55,
      "deletions": 1
    },
    {
      "hash": "52070a799aaed177998f6cf68a4741729b17f4fa",
      "short_hash": "52070a7",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786748847,
      "message": "Routes - Corrige le vrai bug d'affichage des cartes : leaflet.css manquant\n\nLe fix precedent (overflow:hidden + invalidateSize) ne masquait qu'un\nsymptome secondaire. La vraie cause : templates/routes.php ne chargeait\njamais vendor/leaflet/leaflet.css (contrairement a session_detail.php, qui\nle fait deja conditionnellement). Sans cette feuille de style, les tuiles\nLeaflet perdent leur position:absolute et s'empilent dans le flux normal du\ndocument - cartes minuscules mal cadrees, tuiles a cheval avec des\n\"morceaux manquants\". Une hypothese de timing (window load + ResizeObserver)\na ete testee sans effet puis retiree une fois la vraie cause identifiee.\n\nVerifie visuellement sur les 16 vrais parcours de l'utilisateur : les 16\ncartes s'affichent desormais correctement.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Corrige le vrai bug d'affichage des cartes : leaflet.css manquant",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "templates/routes.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 42,
          "deletions": 0,
          "lines_changed": 42
        },
        {
          "path": "templates/routes.php",
          "insertions": 11,
          "deletions": 5,
          "lines_changed": 16
        }
      ],
      "files_changed": 2,
      "insertions": 53,
      "deletions": 5
    },
    {
      "hash": "7796c265cb602edb18ed2de0052755f368a27e39",
      "short_hash": "7796c26",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786748205,
      "message": "Routes - Corrige les vignettes carte qui debordaient (bug, addendum SPOR-413)\n\noverflow:hidden manquant sur .rv-card/.rv-route-thumb (contrairement a la\ncarte unique de session_detail.php qui l'a deja) + map.invalidateSize()\navant fitBounds() pour eviter une mesure de conteneur perimee quand\nplusieurs cartes Leaflet s'initialisent dans une grille CSS pas encore\nstabilisee. Verifie visuellement (Playwright + Edge existant, capture\nd'ecran des 16 vrais parcours du backfill).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Corrige les vignettes carte qui debordaient (bug, addendum SPOR-413)",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "templates/routes.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "templates/routes.php",
          "insertions": 15,
          "deletions": 2,
          "lines_changed": 17
        }
      ],
      "files_changed": 2,
      "insertions": 51,
      "deletions": 2
    },
    {
      "hash": "42e788258228c88bfd5f5d27e4afc86896dd2bc8",
      "short_hash": "42e7882",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786747804,
      "message": "Routes - Backfill des seances historiques (addendum SPOR-412)\n\nRouteMatcher::matchAndLink() n'etait appele qu'a l'import (SessionsController::\npersistSession()) : les seances GPS deja en base avant SPOR-412 n'avaient\njamais ete fingerprintees ni rapprochees, /routes restait vide en usage\nreel. Backfill par lot de 20 (Infra\\Db\\RouteBackfill, meme precaution\nmemoire et meme duo bin/public que ElevationBackfill/SPOR-406), ordonne\nchronologiquement car le matching depend de l'ordre de traitement.\n\nExecute en reel sur ce depot : 150/192 seances GPS fingerprintees, 16\nparcours reels decouverts (le plus emprunte : 10.06 km x12).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Backfill des seances historiques (addendum SPOR-412)",
      "type": "other",
      "modified_files": [
        "TECHNICAL.md",
        "bin/backfill_routes.php",
        "public/backfill_routes.php",
        "src/Infra/Db/RouteBackfill.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "bin/backfill_routes.php",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "public/backfill_routes.php",
          "insertions": 95,
          "deletions": 0,
          "lines_changed": 95
        },
        {
          "path": "src/Infra/Db/RouteBackfill.php",
          "insertions": 81,
          "deletions": 0,
          "lines_changed": 81
        }
      ],
      "files_changed": 4,
      "insertions": 263,
      "deletions": 0
    },
    {
      "hash": "ac6842356cc2f92457a373e0d2a32a3f0d468c7f",
      "short_hash": "ac68423",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786747256,
      "message": "Routes - Nouveaux segments explores (SPOR-417)\n\nTrace bicolore sur le detail de seance pour un parcours connu : defaut bon\nmarche au chargement (empreinte de parcours densifiee 9->40 points) plus un\nbouton \"Precise comparison\" qui relance une comparaison a la trace complete\ndes autres seances du parcours (RouteNoveltyController, a la demande\nseulement). Choix issu d'un AskUserQuestion (le perimetre etait\nvolontairement laisse flou au grooming) : l'utilisateur a demande l'hybride\nbon marche + bouton precis plutot qu'une seule des deux approches.\n\nAvec cette story, l'epic Routes (SPOR-410) est integralement complet.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Nouveaux segments explores (SPOR-417)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/routes.md",
        "docs/sessions.md",
        "src/Domain/Training/RouteFingerprint.php",
        "src/Domain/Training/RouteSegmentNovelty.php",
        "src/Http/Sessions/RouteNoveltyController.php",
        "src/Http/Sessions/SessionDetailController.php",
        "templates/session_detail.php",
        "tests/Domain/RouteFingerprintTest.php",
        "tests/Domain/RouteSegmentNoveltyTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 95,
          "deletions": 0,
          "lines_changed": 95
        },
        {
          "path": "config/routes.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "docs/routes.md",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Domain/Training/RouteFingerprint.php",
          "insertions": 13,
          "deletions": 2,
          "lines_changed": 15
        },
        {
          "path": "src/Domain/Training/RouteSegmentNovelty.php",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        },
        {
          "path": "src/Http/Sessions/RouteNoveltyController.php",
          "insertions": 93,
          "deletions": 0,
          "lines_changed": 93
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 37,
          "deletions": 1,
          "lines_changed": 38
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 78,
          "deletions": 4,
          "lines_changed": 82
        },
        {
          "path": "tests/Domain/RouteFingerprintTest.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "tests/Domain/RouteSegmentNoveltyTest.php",
          "insertions": 69,
          "deletions": 0,
          "lines_changed": 69
        }
      ],
      "files_changed": 12,
      "insertions": 462,
      "deletions": 10
    },
    {
      "hash": "716502555c406a768620729e3a8f672cd125da45",
      "short_hash": "7165025",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786745473,
      "message": "Routes - Rattachement manuel seance <-> parcours (SPOR-416)\n\nSelecteur \"Route\" dans l'onglet Details de l'edition de seance : corrige les\nfaux negatifs/positifs du rapprochement automatique. Choix incoherent\n(id etranger, ou sport different de celui soumis dans le meme formulaire)\nsilencieusement ignore, jamais une erreur de validation. Badge de lecture\nsur le detail de seance, lien vers la fiche du parcours.\n\nAvec cette story, l'epic Routes - Rapprochement de parcours existants\n(SPOR-410) est complet (SPOR-412 a 416) ; seul SPOR-417 (Could, perimetre\nnon defini) reste ouvert.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Rattachement manuel seance <-> parcours (SPOR-416)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/routes.md",
        "docs/sessions.md",
        "src/Http/Sessions/SessionDetailController.php",
        "templates/session_detail.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        },
        {
          "path": "docs/routes.md",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 83,
          "deletions": 2,
          "lines_changed": 85
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        }
      ],
      "files_changed": 6,
      "insertions": 200,
      "deletions": 4
    },
    {
      "hash": "a1d241eb61f718eb8ce66571cec8401e64db76b6",
      "short_hash": "a1d241e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786744677,
      "message": "Routes - Fiche detail d'un parcours, progression dans le temps (SPOR-415)\n\nGET /routes/{id} : carte agrandie, graphique de progression (duree par\noccurrence - bon proxy de l'allure vu la tolerance de matching sur la\ndistance, evite de melanger des unites differentes par sport sur un meme\naxe) et historique chronologique des seances liees avec allure/FC exactes.\nRenommage accessible depuis les deux ecrans (return_to, meme convention que\ngear_edit_modal.php) - corrige au passage une premiere version qui perdait\nle contexte en renommant depuis la fiche detail.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Fiche detail d'un parcours, progression dans le temps (SPOR-415)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/routes.md",
        "public/assets/js/route-progression-chart.js",
        "src/Http/Routes/RoutesController.php",
        "templates/route_detail.php",
        "templates/routes.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 67,
          "deletions": 0,
          "lines_changed": 67
        },
        {
          "path": "config/routes.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "docs/routes.md",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "public/assets/js/route-progression-chart.js",
          "insertions": 48,
          "deletions": 0,
          "lines_changed": 48
        },
        {
          "path": "src/Http/Routes/RoutesController.php",
          "insertions": 137,
          "deletions": 6,
          "lines_changed": 143
        },
        {
          "path": "templates/route_detail.php",
          "insertions": 142,
          "deletions": 0,
          "lines_changed": 142
        },
        {
          "path": "templates/routes.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        }
      ],
      "files_changed": 8,
      "insertions": 407,
      "deletions": 8
    },
    {
      "hash": "5e33348414a8cbc764f7cf230114f4e0203f8201",
      "short_hash": "5e33348",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786744116,
      "message": "Routes - Nommer/renommer un parcours (SPOR-414)\n\nPOST /routes/{id}/rename : ownership verifiee en une requete, nom normalise\nvia SessionTitle::normalize() (meme contrat que le titre de seance). Modale\n\"Rename...\" par carte sur /routes, meme pattern qu'un item de gear.php.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Nommer/renommer un parcours (SPOR-414)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/routes.md",
        "src/Http/Routes/RoutesController.php",
        "templates/routes.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "config/routes.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "docs/routes.md",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Http/Routes/RoutesController.php",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "templates/routes.php",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        }
      ],
      "files_changed": 6,
      "insertions": 117,
      "deletions": 1
    },
    {
      "hash": "a9edb9faa7b10c1b2b8fe8b3b38ab113e023ecf5",
      "short_hash": "a9edb9f",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786743803,
      "message": "Routes - Page dediee /routes (SPOR-413)\n\nListe des parcours \"decouverts\" automatiquement (SPOR-412) : cartes avec\nvignette carte (empreinte legere reutilisee, pas de reparsing), distance,\ndenivele, nombre d'occurrences. Entree sidebar promue de placeholder v2 a\nlien reel. Cartes non cliquables dans ce lot (fiche detail = SPOR-415).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Page dediee /routes (SPOR-413)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/README.md",
        "docs/routes.md",
        "src/Http/Routes/RoutesController.php",
        "templates/partials/app_sidebar.php",
        "templates/routes.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": "config/routes.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "docs/README.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "docs/routes.md",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        },
        {
          "path": "src/Http/Routes/RoutesController.php",
          "insertions": 108,
          "deletions": 0,
          "lines_changed": 108
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/routes.php",
          "insertions": 119,
          "deletions": 0,
          "lines_changed": 119
        }
      ],
      "files_changed": 8,
      "insertions": 327,
      "deletions": 2
    },
    {
      "hash": "738feef362cf99d5a82ce40dd9b92dbbae1a964a",
      "short_hash": "738feef",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786742527,
      "message": "Routes - Schema + detection automatique d'un parcours recurrent (SPOR-412)\n\nEmpreinte geometrique legere (depart/arrivee/distance + points echantillonnes\npar fraction de distance) persistee a l'import, comparee aux parcours et\nseances existants pour rattacher automatiquement une seance a un parcours\nrecurrent des sa 2e occurrence - sans jamais reparser le XML brut ni\npersister de trace complete. Fondation de l'epic Routes (SPOR-410) ; pas\nencore de surface utilisateur (page /routes = SPOR-413).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Routes - Schema + detection automatique d'un parcours recurrent (SPOR-412)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/080_add_routes.sql",
        "src/Domain/Training/RouteFingerprint.php",
        "src/Domain/Training/RouteMatch.php",
        "src/Http/Sessions/RouteMatcher.php",
        "src/Http/Sessions/SessionsController.php",
        "tests/Domain/RouteFingerprintTest.php",
        "tests/Domain/RouteMatchTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 106,
          "deletions": 0,
          "lines_changed": 106
        },
        {
          "path": "db/migrations/080_add_routes.sql",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "src/Domain/Training/RouteFingerprint.php",
          "insertions": 124,
          "deletions": 0,
          "lines_changed": 124
        },
        {
          "path": "src/Domain/Training/RouteMatch.php",
          "insertions": 74,
          "deletions": 0,
          "lines_changed": 74
        },
        {
          "path": "src/Http/Sessions/RouteMatcher.php",
          "insertions": 123,
          "deletions": 0,
          "lines_changed": 123
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "tests/Domain/RouteFingerprintTest.php",
          "insertions": 113,
          "deletions": 0,
          "lines_changed": 113
        },
        {
          "path": "tests/Domain/RouteMatchTest.php",
          "insertions": 96,
          "deletions": 0,
          "lines_changed": 96
        }
      ],
      "files_changed": 9,
      "insertions": 680,
      "deletions": 1
    },
    {
      "hash": "1a811ff4c9fad0e1684846bfbd3c3344fa35aa94",
      "short_hash": "1a811ff",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786741687,
      "message": "Mindmap - Grooming epic Routes (SPOR-410 a 418)\n\nDecoupage en deux epics distincts suite a une divergence identifiee entre la\nmindmap (rapprochement automatique de parcours deja enregistres) et la\nmaquette routes.html (generateur de trace type Strava/Komoot, bloque par la\ncontrainte FTP-only/pas de workers de CLAUDE.md). Rien d'implemente cote\ncode dans cette passe, uniquement le decoupage Jira + mindmap.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Mindmap - Grooming epic Routes (SPOR-410 a 418)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 20,
          "deletions": 1,
          "lines_changed": 21
        }
      ],
      "files_changed": 1,
      "insertions": 20,
      "deletions": 1
    },
    {
      "hash": "6285d130e67d51341254a3b4b980864c9e0c93ec",
      "short_hash": "6285d13",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786740171,
      "message": "Sessions - Signalement de douleur suggere apres import/edition (SPOR-380)\n\nCTA \"Log any pain for this session\" sur le flash d'avertissement SPOR-361/362\n(seance anormalement longue/loin) + bouton \"Report pain...\" toujours visible\nsur le detail de seance : les deux ouvrent directement l'onglet Injury de la\nmodale d'edition, sans dupliquer la saisie body-map deja livree en SPOR-378.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Sessions - Signalement de douleur suggere apres import/edition (SPOR-380)",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/components.css",
        "References/design/assets/js/interactions.js",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/pain-injuries.md",
        "docs/sessions.md",
        "public/assets/css/components.css",
        "public/assets/js/interactions.js",
        "src/Http/Flash.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionsController.php",
        "templates/session_detail.php",
        "templates/sessions.php",
        "tests/Http/FlashTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/components.css",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "References/design/assets/js/interactions.js",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 109,
          "deletions": 0,
          "lines_changed": 109
        },
        {
          "path": "docs/pain-injuries.md",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "docs/sessions.md",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "src/Http/Flash.php",
          "insertions": 16,
          "deletions": 3,
          "lines_changed": 19
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 15,
          "deletions": 1,
          "lines_changed": 16
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 17,
          "deletions": 3,
          "lines_changed": 20
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 25,
          "deletions": 3,
          "lines_changed": 28
        },
        {
          "path": "templates/sessions.php",
          "insertions": 7,
          "deletions": 2,
          "lines_changed": 9
        },
        {
          "path": "tests/Http/FlashTest.php",
          "insertions": 56,
          "deletions": 0,
          "lines_changed": 56
        }
      ],
      "files_changed": 14,
      "insertions": 290,
      "deletions": 14
    },
    {
      "hash": "60d512bcab20e77024fd3b9645bb1b253b51cc4e",
      "short_hash": "60d512b",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786697294,
      "message": "Trends - Histogramme de denivele par sport, exclu pour la Natation (SPOR-408/409)\n\nAjoute un 3e graphique Elevation par sport sur /trends (meme toggle\nSemaine/Mois que Distance/Duration) : VolumeHistory/VolumeSeries\npropagent desormais le denivele bucket par bucket. Le denivele d'un\ntriathlon reste attribue a la seance entiere, jamais reparti par\nsegment (limite structurelle de SPOR-406).\n\nCorrection immediate (SPOR-409) : le graphique reutilisait a tort le\ngating de Distance (Run/Bike/Swim/Triathlon) alors que le denivele n'a\npas de sens pour la Natation - nouveau flag supportsElevation\n(Run/Bike/Triathlon uniquement).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Trends - Histogramme de denivele par sport, exclu pour la Natation (SPOR-408/409)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/statistiques.md",
        "src/Domain/Training/VolumeSeries.php",
        "src/Http/Training/VolumeHistory.php",
        "src/Http/Trends/TrendsController.php",
        "templates/trends.php",
        "tests/Domain/VolumeSeriesTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 55,
          "deletions": 0,
          "lines_changed": 55
        },
        {
          "path": "docs/statistiques.md",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "src/Domain/Training/VolumeSeries.php",
          "insertions": 11,
          "deletions": 3,
          "lines_changed": 14
        },
        {
          "path": "src/Http/Training/VolumeHistory.php",
          "insertions": 15,
          "deletions": 6,
          "lines_changed": 21
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 13,
          "deletions": 7,
          "lines_changed": 20
        },
        {
          "path": "templates/trends.php",
          "insertions": 24,
          "deletions": 3,
          "lines_changed": 27
        },
        {
          "path": "tests/Domain/VolumeSeriesTest.php",
          "insertions": 49,
          "deletions": 0,
          "lines_changed": 49
        }
      ],
      "files_changed": 8,
      "insertions": 172,
      "deletions": 20
    },
    {
      "hash": "1b212371a7ea8c69f4d7c2e1089c3a3cb59d0332",
      "short_hash": "1b21237",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786696168,
      "message": "Zones de puissance, cartographie douleur/blessures, rappels avances, tendances - totaux mensuels (SPOR-370 a 407)\n\nBundle plusieurs epics livres depuis le dernier commit :\n- Zones de puissance configurables par sport + Time in zone (SPOR-370)\n- Pain & Injuries : cartographie corporelle, page dediee, historique\n  date, check-ins, courbes de tendance (SPOR-373 a 402)\n- Rappels : mesure liee, alertes hebdo/mensuelles (SPOR-373-375 db)\n- Calendrier : courses planifiees + resume mensuel (SPOR-381/382)\n- Login : ecran verrouille corrige (SPOR-404)\n- Trends : tableau des totaux mensuels tous sports + denivele positif\n  persiste a l'import avec backfill (SPOR-405/406/407)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Zones de puissance, cartographie douleur/blessures, rappels avances, tendances - totaux mensuels (SP",
      "type": "other",
      "modified_files": [
        "CLAUDE.md",
        "References/design/app/health.html",
        "References/design/app/pain-injuries.html",
        "References/design/app/session-detail.html",
        "References/design/assets/css/app.css",
        "References/design/assets/css/components.css",
        "References/design/assets/js/interactions.js",
        "References/design/assets/js/viz.js",
        "References/mindmap/Sport Log.md",
        "References/pain-injury-mockup/Body Map.dc.html",
        "References/pain-injury-mockup/README.md",
        "References/pain-injury-mockup/support.js",
        "TECHNICAL.md",
        "bin/backfill_elevation.php",
        "config/routes.php",
        "db/migrations/073_add_reminder_linked_measure_type.sql",
        "db/migrations/074_add_weekly_reminder_alert.sql",
        "db/migrations/075_add_monthly_reminder_alert.sql",
        "db/migrations/076_add_power_zone_config.sql",
        "db/migrations/077_add_body_map_pain_injuries.sql",
        "db/migrations/078_add_injury_intensity_tissue_sensation.sql",
        "db/migrations/079_add_session_elevation_gain.sql",
        "docs/README.md",
        "docs/admin.md",
        "docs/auth.md",
        "docs/badges.md",
        "docs/calendrier.md",
        "docs/dashboard.md",
        "docs/materiel.md",
        "docs/notifications.md",
        "docs/objectifs.md",
        "docs/pain-injuries.md",
        "docs/profil.md",
        "docs/records.md",
        "docs/sante.md",
        "docs/sessions.md",
        "docs/site.md",
        "docs/statistiques.md",
        "public/assets/css/app.css",
        "public/assets/css/components.css",
        "public/assets/js/icons.js",
        "public/assets/js/injury-timeline-chart.js",
        "public/assets/js/interactions.js",
        "public/assets/js/pain-load-chart.js",
        "public/backfill_elevation.php",
        "src/Domain/Health/BloodPressureCategory.php",
        "src/Domain/Health/BodyRegion.php",
        "src/Domain/Health/Injury.php",
        "src/Domain/Health/InjuryEvolutionStatus.php",
        "src/Domain/Health/InjuryPainLog.php",
        "src/Domain/Health/PainLoadWeeklySeries.php",
        "src/Domain/Health/Reminder.php",
        "src/Domain/Health/SessionPainMark.php",
        "src/Domain/Import/ElevationGain.php",
        "src/Domain/Import/SessionData.php",
        "src/Domain/Physiology/PowerZoneConfig.php",
        "src/Domain/Physiology/PowerZoneMethod.php",
        "src/Domain/Physiology/PowerZoneTimeDistribution.php",
        "src/Domain/Physiology/PowerZones.php",
        "src/Domain/Training/MonthlyTotalsSeries.php",
        "src/Domain/Training/RaceCalendarGrouper.php",
        "src/Domain/Training/SessionSensations.php",
        "src/Domain/Training/SessionTagsInput.php",
        "src/Http/Admin/AdminSessionTagsController.php",
        "src/Http/Auth/LoginController.php",
        "src/Http/Calendar/CalendarController.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/PainInjuriesController.php",
        "src/Http/Notifications/NotificationGenerator.php",
        "src/Http/Physiology/BestObservedPower.php",
        "src/Http/Physiology/PowerZoneConfigFactory.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionPresenter.php",
        "src/Http/Sessions/SessionTagsStore.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Http/Settings/SettingsController.php",
        "src/Http/Trends/MonthlyTotals.php",
        "src/Http/Trends/TrendsController.php",
        "src/Infra/Db/ElevationBackfill.php",
        "templates/admin_tags.php",
        "templates/badges.php",
        "templates/calendar.php",
        "templates/health.php",
        "templates/login.php",
        "templates/pain_injuries.php",
        "templates/partials/app_sidebar.php",
        "templates/partials/checklist_edit_modal.php",
        "templates/partials/injury_form_fields.php",
        "templates/partials/reminder_form_fields.php",
        "templates/partials/tag_edit_modal.php",
        "templates/session_detail.php",
        "templates/sessions.php",
        "templates/settings.php",
        "templates/trends.php",
        "tests/Domain/BloodPressureCategoryTest.php",
        "tests/Domain/BodyRegionTest.php",
        "tests/Domain/ElevationGainTest.php",
        "tests/Domain/InjuryPainLogTest.php",
        "tests/Domain/InjuryTest.php",
        "tests/Domain/MonthlyTotalsSeriesTest.php",
        "tests/Domain/PainLoadWeeklySeriesTest.php",
        "tests/Domain/PowerZoneConfigTest.php",
        "tests/Domain/PowerZoneTimeDistributionTest.php",
        "tests/Domain/PowerZonesTest.php",
        "tests/Domain/RaceCalendarGrouperTest.php",
        "tests/Domain/ReminderTest.php",
        "tests/Domain/SessionPainMarkTest.php",
        "tests/Domain/SessionTagsInputTest.php",
        "tests/Http/Physiology/PowerZoneConfigFactoryTest.php",
        "tests/Http/Sessions/SessionPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "CLAUDE.md",
          "insertions": 37,
          "deletions": 2,
          "lines_changed": 39
        },
        {
          "path": "References/design/app/health.html",
          "insertions": 30,
          "deletions": 114,
          "lines_changed": 144
        },
        {
          "path": "References/design/app/pain-injuries.html",
          "insertions": 515,
          "deletions": 0,
          "lines_changed": 515
        },
        {
          "path": "References/design/app/session-detail.html",
          "insertions": 92,
          "deletions": 9,
          "lines_changed": 101
        },
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "References/design/assets/css/components.css",
          "insertions": 198,
          "deletions": 1,
          "lines_changed": 199
        },
        {
          "path": "References/design/assets/js/interactions.js",
          "insertions": 366,
          "deletions": 0,
          "lines_changed": 366
        },
        {
          "path": "References/design/assets/js/viz.js",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 58,
          "deletions": 0,
          "lines_changed": 58
        },
        {
          "path": "References/pain-injury-mockup/Body Map.dc.html",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "References/pain-injury-mockup/README.md",
          "insertions": 1160,
          "deletions": 0,
          "lines_changed": 1160
        },
        {
          "path": "References/pain-injury-mockup/support.js",
          "insertions": 478,
          "deletions": 0,
          "lines_changed": 478
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 2127,
          "deletions": 8,
          "lines_changed": 2135
        },
        {
          "path": "bin/backfill_elevation.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "config/routes.php",
          "insertions": 23,
          "deletions": 6,
          "lines_changed": 29
        },
        {
          "path": "db/migrations/073_add_reminder_linked_measure_type.sql",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "db/migrations/074_add_weekly_reminder_alert.sql",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "db/migrations/075_add_monthly_reminder_alert.sql",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "db/migrations/076_add_power_zone_config.sql",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "db/migrations/077_add_body_map_pain_injuries.sql",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        },
        {
          "path": "db/migrations/078_add_injury_intensity_tissue_sensation.sql",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "db/migrations/079_add_session_elevation_gain.sql",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "docs/README.md",
          "insertions": 48,
          "deletions": 0,
          "lines_changed": 48
        },
        {
          "path": "docs/admin.md",
          "insertions": 105,
          "deletions": 0,
          "lines_changed": 105
        },
        {
          "path": "docs/auth.md",
          "insertions": 59,
          "deletions": 0,
          "lines_changed": 59
        },
        {
          "path": "docs/badges.md",
          "insertions": 59,
          "deletions": 0,
          "lines_changed": 59
        },
        {
          "path": "docs/calendrier.md",
          "insertions": 43,
          "deletions": 0,
          "lines_changed": 43
        },
        {
          "path": "docs/dashboard.md",
          "insertions": 69,
          "deletions": 0,
          "lines_changed": 69
        },
        {
          "path": "docs/materiel.md",
          "insertions": 132,
          "deletions": 0,
          "lines_changed": 132
        },
        {
          "path": "docs/notifications.md",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 144,
          "deletions": 0,
          "lines_changed": 144
        },
        {
          "path": "docs/pain-injuries.md",
          "insertions": 178,
          "deletions": 0,
          "lines_changed": 178
        },
        {
          "path": "docs/profil.md",
          "insertions": 87,
          "deletions": 0,
          "lines_changed": 87
        },
        {
          "path": "docs/records.md",
          "insertions": 67,
          "deletions": 0,
          "lines_changed": 67
        },
        {
          "path": "docs/sante.md",
          "insertions": 143,
          "deletions": 0,
          "lines_changed": 143
        },
        {
          "path": "docs/sessions.md",
          "insertions": 119,
          "deletions": 0,
          "lines_changed": 119
        },
        {
          "path": "docs/site.md",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "docs/statistiques.md",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 133,
          "deletions": 1,
          "lines_changed": 134
        },
        {
          "path": "public/assets/js/icons.js",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "public/assets/js/injury-timeline-chart.js",
          "insertions": 134,
          "deletions": 0,
          "lines_changed": 134
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 578,
          "deletions": 0,
          "lines_changed": 578
        },
        {
          "path": "public/assets/js/pain-load-chart.js",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        },
        {
          "path": "public/backfill_elevation.php",
          "insertions": 94,
          "deletions": 0,
          "lines_changed": 94
        },
        {
          "path": "src/Domain/Health/BloodPressureCategory.php",
          "insertions": 58,
          "deletions": 0,
          "lines_changed": 58
        },
        {
          "path": "src/Domain/Health/BodyRegion.php",
          "insertions": 124,
          "deletions": 0,
          "lines_changed": 124
        },
        {
          "path": "src/Domain/Health/Injury.php",
          "insertions": 34,
          "deletions": 1,
          "lines_changed": 35
        },
        {
          "path": "src/Domain/Health/InjuryEvolutionStatus.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "src/Domain/Health/InjuryPainLog.php",
          "insertions": 107,
          "deletions": 0,
          "lines_changed": 107
        },
        {
          "path": "src/Domain/Health/PainLoadWeeklySeries.php",
          "insertions": 85,
          "deletions": 0,
          "lines_changed": 85
        },
        {
          "path": "src/Domain/Health/Reminder.php",
          "insertions": 83,
          "deletions": 5,
          "lines_changed": 88
        },
        {
          "path": "src/Domain/Health/SessionPainMark.php",
          "insertions": 89,
          "deletions": 0,
          "lines_changed": 89
        },
        {
          "path": "src/Domain/Import/ElevationGain.php",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "src/Domain/Import/SessionData.php",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "src/Domain/Physiology/PowerZoneConfig.php",
          "insertions": 77,
          "deletions": 0,
          "lines_changed": 77
        },
        {
          "path": "src/Domain/Physiology/PowerZoneMethod.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "src/Domain/Physiology/PowerZoneTimeDistribution.php",
          "insertions": 54,
          "deletions": 0,
          "lines_changed": 54
        },
        {
          "path": "src/Domain/Physiology/PowerZones.php",
          "insertions": 136,
          "deletions": 0,
          "lines_changed": 136
        },
        {
          "path": "src/Domain/Training/MonthlyTotalsSeries.php",
          "insertions": 60,
          "deletions": 0,
          "lines_changed": 60
        },
        {
          "path": "src/Domain/Training/RaceCalendarGrouper.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "src/Domain/Training/SessionSensations.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Domain/Training/SessionTagsInput.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "src/Http/Admin/AdminSessionTagsController.php",
          "insertions": 94,
          "deletions": 56,
          "lines_changed": 150
        },
        {
          "path": "src/Http/Auth/LoginController.php",
          "insertions": 18,
          "deletions": 1,
          "lines_changed": 19
        },
        {
          "path": "src/Http/Calendar/CalendarController.php",
          "insertions": 80,
          "deletions": 4,
          "lines_changed": 84
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 66,
          "deletions": 537,
          "lines_changed": 603
        },
        {
          "path": "src/Http/Health/PainInjuriesController.php",
          "insertions": 1296,
          "deletions": 0,
          "lines_changed": 1296
        },
        {
          "path": "src/Http/Notifications/NotificationGenerator.php",
          "insertions": 154,
          "deletions": 0,
          "lines_changed": 154
        },
        {
          "path": "src/Http/Physiology/BestObservedPower.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "src/Http/Physiology/PowerZoneConfigFactory.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 189,
          "deletions": 8,
          "lines_changed": 197
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 81,
          "deletions": 14,
          "lines_changed": 95
        },
        {
          "path": "src/Http/Sessions/SessionTagsStore.php",
          "insertions": 118,
          "deletions": 0,
          "lines_changed": 118
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 68,
          "deletions": 2,
          "lines_changed": 70
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 301,
          "deletions": 4,
          "lines_changed": 305
        },
        {
          "path": "src/Http/Trends/MonthlyTotals.php",
          "insertions": 43,
          "deletions": 0,
          "lines_changed": 43
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "src/Infra/Db/ElevationBackfill.php",
          "insertions": 71,
          "deletions": 0,
          "lines_changed": 71
        },
        {
          "path": "templates/admin_tags.php",
          "insertions": 26,
          "deletions": 25,
          "lines_changed": 51
        },
        {
          "path": "templates/badges.php",
          "insertions": 17,
          "deletions": 3,
          "lines_changed": 20
        },
        {
          "path": "templates/calendar.php",
          "insertions": 40,
          "deletions": 2,
          "lines_changed": 42
        },
        {
          "path": "templates/health.php",
          "insertions": 35,
          "deletions": 194,
          "lines_changed": 229
        },
        {
          "path": "templates/login.php",
          "insertions": 54,
          "deletions": 3,
          "lines_changed": 57
        },
        {
          "path": "templates/pain_injuries.php",
          "insertions": 635,
          "deletions": 0,
          "lines_changed": 635
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 11,
          "deletions": 5,
          "lines_changed": 16
        },
        {
          "path": "templates/partials/checklist_edit_modal.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/partials/injury_form_fields.php",
          "insertions": 73,
          "deletions": 1,
          "lines_changed": 74
        },
        {
          "path": "templates/partials/reminder_form_fields.php",
          "insertions": 22,
          "deletions": 1,
          "lines_changed": 23
        },
        {
          "path": "templates/partials/tag_edit_modal.php",
          "insertions": 0,
          "deletions": 45,
          "lines_changed": 45
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 332,
          "deletions": 26,
          "lines_changed": 358
        },
        {
          "path": "templates/sessions.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "templates/settings.php",
          "insertions": 209,
          "deletions": 2,
          "lines_changed": 211
        },
        {
          "path": "templates/trends.php",
          "insertions": 40,
          "deletions": 1,
          "lines_changed": 41
        },
        {
          "path": "tests/Domain/BloodPressureCategoryTest.php",
          "insertions": 82,
          "deletions": 0,
          "lines_changed": 82
        },
        {
          "path": "tests/Domain/BodyRegionTest.php",
          "insertions": 68,
          "deletions": 0,
          "lines_changed": 68
        },
        {
          "path": "tests/Domain/ElevationGainTest.php",
          "insertions": 58,
          "deletions": 0,
          "lines_changed": 58
        },
        {
          "path": "tests/Domain/InjuryPainLogTest.php",
          "insertions": 137,
          "deletions": 0,
          "lines_changed": 137
        },
        {
          "path": "tests/Domain/InjuryTest.php",
          "insertions": 72,
          "deletions": 0,
          "lines_changed": 72
        },
        {
          "path": "tests/Domain/MonthlyTotalsSeriesTest.php",
          "insertions": 59,
          "deletions": 0,
          "lines_changed": 59
        },
        {
          "path": "tests/Domain/PainLoadWeeklySeriesTest.php",
          "insertions": 106,
          "deletions": 0,
          "lines_changed": 106
        },
        {
          "path": "tests/Domain/PowerZoneConfigTest.php",
          "insertions": 79,
          "deletions": 0,
          "lines_changed": 79
        },
        {
          "path": "tests/Domain/PowerZoneTimeDistributionTest.php",
          "insertions": 73,
          "deletions": 0,
          "lines_changed": 73
        },
        {
          "path": "tests/Domain/PowerZonesTest.php",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "tests/Domain/RaceCalendarGrouperTest.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "tests/Domain/ReminderTest.php",
          "insertions": 133,
          "deletions": 0,
          "lines_changed": 133
        },
        {
          "path": "tests/Domain/SessionPainMarkTest.php",
          "insertions": 73,
          "deletions": 0,
          "lines_changed": 73
        },
        {
          "path": "tests/Domain/SessionTagsInputTest.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "tests/Http/Physiology/PowerZoneConfigFactoryTest.php",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 95,
          "deletions": 0,
          "lines_changed": 95
        }
      ],
      "files_changed": 110,
      "insertions": 14179,
      "deletions": 1083
    },
    {
      "hash": "89ceff676d0a033d6e2b689cdd785c3933748ecb",
      "short_hash": "89ceff6",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786271562,
      "message": "Sante - Tableau des mesures par type avec edition/suppression (SPOR-363)\n\nSur /health, carte \"All measures\" independante (onglet par HealthMeasureType,\npagination facon /sessions, edition et suppression par ligne) en plus de la\nmodale Progression existante, conservee telle quelle. Repository etendu\n(findById/countByType/fetchPageByType/update/delete), formatage de ligne\nextrait dans MeasureRowPresenter (teste, 4 cas), suite complete verte\n(770 tests). Verifie manuellement via session PHP forgee pour CurlRunner.\n\nPassage : nettoyage de l'epic Objectifs (SPOR-38, stories Draft/WF\nobsoletes du workflow d'origine cloturees) dans la mindmap.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Sante - Tableau des mesures par type avec edition/suppression (SPOR-363)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/HealthMeasureRepository.php",
        "src/Http/Health/MeasureRowPresenter.php",
        "templates/health.php",
        "tests/Http/Health/MeasureRowPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 9,
          "deletions": 8,
          "lines_changed": 17
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 154,
          "deletions": 0,
          "lines_changed": 154
        },
        {
          "path": "config/routes.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 225,
          "deletions": 19,
          "lines_changed": 244
        },
        {
          "path": "src/Http/Health/HealthMeasureRepository.php",
          "insertions": 100,
          "deletions": 7,
          "lines_changed": 107
        },
        {
          "path": "src/Http/Health/MeasureRowPresenter.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "templates/health.php",
          "insertions": 142,
          "deletions": 0,
          "lines_changed": 142
        },
        {
          "path": "tests/Http/Health/MeasureRowPresenterTest.php",
          "insertions": 77,
          "deletions": 0,
          "lines_changed": 77
        }
      ],
      "files_changed": 8,
      "insertions": 754,
      "deletions": 34
    },
    {
      "hash": "92bbb9a6662c5880c5e585d92e7fa3da6711a4cb",
      "short_hash": "92bbb9a",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786267220,
      "message": "Import - Avertissement si distance/duree anormalement elevee pour le sport (SPOR-362)\n\nCompare chaque nouvelle seance (import ou saisie manuelle) a la moyenne\ndes seances anterieures du meme sport ; au-dela de 2x/4x la moyenne, le\nflash de succes est degrade en avertissement non bloquant plutot que de\nrejeter ou corriger la seance silencieusement.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Import - Avertissement si distance/duree anormalement elevee pour le sport (SPOR-362)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "src/Domain/Training/ImportAnomalyCheck.php",
        "src/Http/Sessions/SessionsController.php",
        "tests/Domain/ImportAnomalyCheckTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 63,
          "deletions": 0,
          "lines_changed": 63
        },
        {
          "path": "src/Domain/Training/ImportAnomalyCheck.php",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 45,
          "deletions": 4,
          "lines_changed": 49
        },
        {
          "path": "tests/Domain/ImportAnomalyCheckTest.php",
          "insertions": 94,
          "deletions": 0,
          "lines_changed": 94
        }
      ],
      "files_changed": 5,
      "insertions": 269,
      "deletions": 4
    },
    {
      "hash": "11f17f58c4719e4fd3403f545d6be052a67d25b9",
      "short_hash": "11f17f5",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786266649,
      "message": "Checklist de preparation course, Sensations de seance, selecteur mois/annee calendrier (SPOR-345 a 360)\n\nTrois lots finis mais restes non commites : catalogue admin + items\ncustom + checklist par course (SPOR-345/346/347/348/349/350), Feeling/\nMotivation/Douleur sur les seances (SPOR-355/356/357/358), correction de\ncasse des libelles hexagones et navigation mois/annee du calendrier\n(SPOR-359/360). Toutes deja transitionnees Done sur Jira, docs\nTECHNICAL.md et mindmap deja a jour.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Checklist de preparation course, Sensations de seance, selecteur mois/annee calendrier (SPOR-345 a 3",
      "type": "other",
      "modified_files": [
        ".gitignore",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/068_add_race_checklist_catalog.sql",
        "db/migrations/069_add_race_checklist_item_ids.sql",
        "db/migrations/070_add_race_checklist_custom_items.sql",
        "db/migrations/071_add_race_checklist_enabled_removed.sql",
        "db/migrations/072_add_session_sensations.sql",
        "public/assets/css/app.css",
        "public/assets/js/dashboard-charts.js",
        "public/assets/js/interactions.js",
        "src/Domain/Training/CalendarYearRange.php",
        "src/Domain/Training/ChecklistCustomItems.php",
        "src/Domain/Training/ChecklistSelection.php",
        "src/Domain/Training/HexLabel.php",
        "src/Domain/Training/SessionSensations.php",
        "src/Domain/Training/VolumeComparison.php",
        "src/Domain/Training/WeeklySportBreakdown.php",
        "src/Http/Admin/AdminChecklistController.php",
        "src/Http/Calendar/CalendarController.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Goals/GoalFormHelpers.php",
        "src/Http/Goals/RaceChecklistStore.php",
        "src/Http/Goals/RacesController.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Trends/TrendsController.php",
        "templates/admin_checklist.php",
        "templates/calendar.php",
        "templates/dashboard.php",
        "templates/partials/admin_tabs.php",
        "templates/partials/checklist_catalog_chip.php",
        "templates/partials/checklist_edit_modal.php",
        "templates/partials/checklist_form_fields.php",
        "templates/partials/goal_form_fields.php",
        "templates/races.php",
        "templates/session_detail.php",
        "tests/Domain/CalendarYearRangeTest.php",
        "tests/Domain/ChecklistCustomItemsTest.php",
        "tests/Domain/ChecklistSelectionTest.php",
        "tests/Domain/HexLabelTest.php",
        "tests/Domain/SessionSensationsTest.php",
        "tests/Domain/VolumeComparisonTest.php",
        "tests/Domain/WeeklySportBreakdownTest.php"
      ],
      "modified_files_details": [
        {
          "path": ".gitignore",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 913,
          "deletions": 7,
          "lines_changed": 920
        },
        {
          "path": "config/routes.php",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "db/migrations/068_add_race_checklist_catalog.sql",
          "insertions": 47,
          "deletions": 0,
          "lines_changed": 47
        },
        {
          "path": "db/migrations/069_add_race_checklist_item_ids.sql",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "db/migrations/070_add_race_checklist_custom_items.sql",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "db/migrations/071_add_race_checklist_enabled_removed.sql",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "db/migrations/072_add_session_sensations.sql",
          "insertions": 22,
          "deletions": 0,
          "lines_changed": 22
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 29,
          "deletions": 3,
          "lines_changed": 32
        },
        {
          "path": "public/assets/js/dashboard-charts.js",
          "insertions": 62,
          "deletions": 24,
          "lines_changed": 86
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 189,
          "deletions": 0,
          "lines_changed": 189
        },
        {
          "path": "src/Domain/Training/CalendarYearRange.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "src/Domain/Training/ChecklistCustomItems.php",
          "insertions": 62,
          "deletions": 0,
          "lines_changed": 62
        },
        {
          "path": "src/Domain/Training/ChecklistSelection.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "src/Domain/Training/HexLabel.php",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "src/Domain/Training/SessionSensations.php",
          "insertions": 62,
          "deletions": 0,
          "lines_changed": 62
        },
        {
          "path": "src/Domain/Training/VolumeComparison.php",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "src/Domain/Training/WeeklySportBreakdown.php",
          "insertions": 59,
          "deletions": 0,
          "lines_changed": 59
        },
        {
          "path": "src/Http/Admin/AdminChecklistController.php",
          "insertions": 251,
          "deletions": 0,
          "lines_changed": 251
        },
        {
          "path": "src/Http/Calendar/CalendarController.php",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 89,
          "deletions": 32,
          "lines_changed": 121
        },
        {
          "path": "src/Http/Goals/GoalFormHelpers.php",
          "insertions": 65,
          "deletions": 2,
          "lines_changed": 67
        },
        {
          "path": "src/Http/Goals/RaceChecklistStore.php",
          "insertions": 91,
          "deletions": 0,
          "lines_changed": 91
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 119,
          "deletions": 8,
          "lines_changed": 127
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 115,
          "deletions": 2,
          "lines_changed": 117
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 14,
          "deletions": 4,
          "lines_changed": 18
        },
        {
          "path": "templates/admin_checklist.php",
          "insertions": 129,
          "deletions": 0,
          "lines_changed": 129
        },
        {
          "path": "templates/calendar.php",
          "insertions": 70,
          "deletions": 2,
          "lines_changed": 72
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 89,
          "deletions": 15,
          "lines_changed": 104
        },
        {
          "path": "templates/partials/admin_tabs.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "templates/partials/checklist_catalog_chip.php",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        },
        {
          "path": "templates/partials/checklist_edit_modal.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "templates/partials/checklist_form_fields.php",
          "insertions": 49,
          "deletions": 0,
          "lines_changed": 49
        },
        {
          "path": "templates/partials/goal_form_fields.php",
          "insertions": 153,
          "deletions": 21,
          "lines_changed": 174
        },
        {
          "path": "templates/races.php",
          "insertions": 11,
          "deletions": 1,
          "lines_changed": 12
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 103,
          "deletions": 4,
          "lines_changed": 107
        },
        {
          "path": "tests/Domain/CalendarYearRangeTest.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "tests/Domain/ChecklistCustomItemsTest.php",
          "insertions": 77,
          "deletions": 0,
          "lines_changed": 77
        },
        {
          "path": "tests/Domain/ChecklistSelectionTest.php",
          "insertions": 107,
          "deletions": 0,
          "lines_changed": 107
        },
        {
          "path": "tests/Domain/HexLabelTest.php",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "tests/Domain/SessionSensationsTest.php",
          "insertions": 69,
          "deletions": 0,
          "lines_changed": 69
        },
        {
          "path": "tests/Domain/VolumeComparisonTest.php",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "tests/Domain/WeeklySportBreakdownTest.php",
          "insertions": 64,
          "deletions": 0,
          "lines_changed": 64
        }
      ],
      "files_changed": 44,
      "insertions": 3539,
      "deletions": 126
    },
    {
      "hash": "ccab98c61200590cea2eb4bc690c84c82729e9d3",
      "short_hash": "ccab98c",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1786139659,
      "message": "Tags de seance, blessures avancees, zones FC configurables, medias multiples (SPOR-319 a 344)\n\nGros lot de features documentees dans TECHNICAL.md et la mindmap mais pas\nencore committees :\n\n- Sessions - Tags de seance : catalogue admin, saisie catalogue+libre,\n  badge+tooltip sur /sessions et dashboard (SPOR-319/320/321/322/323)\n- Sessions - Puissance et cadence (import FIT/TCX), min/max sur les tuiles\n  Metrics, badge equipement sur /sessions (SPOR-325/327/331/332/334)\n- Sante - Edition de blessure (date de fin), liaison a plusieurs seances,\n  badge blessure sur la seance liee, onglets Details/Sessions, modale\n  Progression, pastille sidebar, suppression avec confirmation\n  (SPOR-336 a 342)\n- Profil - Photo de profil, zones FC configurables (Karvonen/%FCmax/Manuel,\n  3-8 zones) (SPOR-328/333)\n- Medias - Visibilite publique par media, badge avec compte, nettoyage\n  metadata video, galerie multi-media materiel (SPOR-297/324/343)\n- Corrections : import FIT sans sport reconnu (SPOR-326), badges sport\n  illisibles (SPOR-344), filtre sport sur \"Suggested next\" (SPOR-329),\n  dupliquer un badge admin (SPOR-335), separateur par jour dashboard\n  (SPOR-330)\n- Fixtures References/dataset/*.fit anonymisees (GPS translate vers la\n  Laponie avec rescale cos(lat), serial_number neutralise, CRC recalcule)\n  avant commit, meme politique que les fixtures TCX existantes\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Tags de seance, blessures avancees, zones FC configurables, medias multiples (SPOR-319 a 344)",
      "type": "other",
      "modified_files": [
        "References/dataset/23764794862_ACTIVITY.fit",
        "References/dataset/FORM_2026-08-01_123709.fit",
        "References/design/assets/css/app.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/060_add_session_media_visibility.sql",
        "db/migrations/061_add_session_tag_catalog.sql",
        "db/migrations/062_add_session_tags.sql",
        "db/migrations/063_add_session_avg_power.sql",
        "db/migrations/064_add_user_avatar.sql",
        "db/migrations/065_add_heart_rate_zone_config.sql",
        "db/migrations/066_add_injury_sessions.sql",
        "db/migrations/067_add_gear_media.sql",
        "public/assets/css/app.css",
        "public/assets/css/components.css",
        "public/assets/css/tokens.css",
        "public/assets/js/health-trends.js",
        "public/assets/js/icons.js",
        "public/assets/js/interactions.js",
        "public/assets/js/session-charts.js",
        "src/Domain/Health/Injury.php",
        "src/Domain/Import/FitImporter.php",
        "src/Domain/Import/SessionData.php",
        "src/Domain/Import/TcxImporter.php",
        "src/Domain/Import/TrackpointData.php",
        "src/Domain/Physiology/HeartRateZoneBoundsGenerator.php",
        "src/Domain/Physiology/HeartRateZoneConfig.php",
        "src/Domain/Physiology/HeartRateZoneMethod.php",
        "src/Domain/Physiology/HeartRateZones.php",
        "src/Domain/Training/GearPhoto.php",
        "src/Domain/Training/SessionMediaUpload.php",
        "src/Domain/Training/SessionTagsInput.php",
        "src/Domain/Training/VideoMetadataStripper.php",
        "src/Http/Admin/AdminBadgesController.php",
        "src/Http/Admin/AdminQuotesController.php",
        "src/Http/Admin/AdminSessionTagsController.php",
        "src/Http/Admin/AdminSportsController.php",
        "src/Http/Admin/BadgeDuplicateImage.php",
        "src/Http/Admin/InvitationsController.php",
        "src/Http/Admin/LogsController.php",
        "src/Http/Calendar/CalendarController.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Gear/GearAutoAssignController.php",
        "src/Http/Gear/GearController.php",
        "src/Http/Gear/GearMediaController.php",
        "src/Http/Gear/GearMediaStore.php",
        "src/Http/Goals/GoalsController.php",
        "src/Http/Goals/RacesController.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/InjurySidebarCounter.php",
        "src/Http/Notifications/NotificationsController.php",
        "src/Http/Physiology/HeartRateZoneConfigFactory.php",
        "src/Http/Profile/ProfileController.php",
        "src/Http/Records/BadgesController.php",
        "src/Http/Records/RecordsController.php",
        "src/Http/Sessions/RaceResultStore.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionGearStore.php",
        "src/Http/Sessions/SessionInjuryStore.php",
        "src/Http/Sessions/SessionMediaController.php",
        "src/Http/Sessions/SessionMediaStore.php",
        "src/Http/Sessions/SessionPresenter.php",
        "src/Http/Sessions/SessionTagsStore.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Http/Settings/SettingsController.php",
        "src/Http/Trends/TrendsController.php",
        "src/Infra/View/Html.php",
        "templates/admin_badges.php",
        "templates/admin_tags.php",
        "templates/dashboard.php",
        "templates/gear.php",
        "templates/gear_detail.php",
        "templates/goals.php",
        "templates/health.php",
        "templates/partials/admin_tabs.php",
        "templates/partials/app_sidebar.php",
        "templates/partials/app_topbar.php",
        "templates/partials/badge_form_fields.php",
        "templates/partials/gear_edit_modal.php",
        "templates/partials/gear_form_fields.php",
        "templates/partials/injury_form_fields.php",
        "templates/partials/tag_edit_modal.php",
        "templates/partials/tag_form_fields.php",
        "templates/profile.php",
        "templates/races.php",
        "templates/records.php",
        "templates/session_detail.php",
        "templates/sessions.php",
        "templates/settings.php",
        "tests/Domain/FitImporterTest.php",
        "tests/Domain/HeartRateZoneBoundsGeneratorTest.php",
        "tests/Domain/HeartRateZoneConfigTest.php",
        "tests/Domain/HeartRateZonesTest.php",
        "tests/Domain/InjuryTest.php",
        "tests/Domain/SessionMediaUploadTest.php",
        "tests/Domain/SessionTagsInputTest.php",
        "tests/Domain/TcxImporterTest.php",
        "tests/Domain/VideoMetadataStripperTest.php",
        "tests/Http/Admin/BadgeDuplicateImageTest.php",
        "tests/Http/Physiology/HeartRateZoneConfigFactoryTest.php",
        "tests/Http/Sessions/SessionPresenterTest.php",
        "tests/Infra/View/HtmlTest.php",
        "tests/fixtures/sample_cadence.tcx",
        "tests/fixtures/sample_power.tcx"
      ],
      "modified_files_details": [
        {
          "path": "References/dataset/23764794862_ACTIVITY.fit",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "References/dataset/FORM_2026-08-01_123709.fit",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 36,
          "deletions": 1,
          "lines_changed": 37
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 1825,
          "deletions": 10,
          "lines_changed": 1835
        },
        {
          "path": "config/routes.php",
          "insertions": 28,
          "deletions": 1,
          "lines_changed": 29
        },
        {
          "path": "db/migrations/060_add_session_media_visibility.sql",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "db/migrations/061_add_session_tag_catalog.sql",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "db/migrations/062_add_session_tags.sql",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "db/migrations/063_add_session_avg_power.sql",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "db/migrations/064_add_user_avatar.sql",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "db/migrations/065_add_heart_rate_zone_config.sql",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "db/migrations/066_add_injury_sessions.sql",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "db/migrations/067_add_gear_media.sql",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 23,
          "deletions": 1,
          "lines_changed": 24
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "public/assets/css/tokens.css",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "public/assets/js/health-trends.js",
          "insertions": 72,
          "deletions": 4,
          "lines_changed": 76
        },
        {
          "path": "public/assets/js/icons.js",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 143,
          "deletions": 0,
          "lines_changed": 143
        },
        {
          "path": "public/assets/js/session-charts.js",
          "insertions": 5,
          "deletions": 2,
          "lines_changed": 7
        },
        {
          "path": "src/Domain/Health/Injury.php",
          "insertions": 8,
          "deletions": 1,
          "lines_changed": 9
        },
        {
          "path": "src/Domain/Import/FitImporter.php",
          "insertions": 16,
          "deletions": 6,
          "lines_changed": 22
        },
        {
          "path": "src/Domain/Import/SessionData.php",
          "insertions": 8,
          "deletions": 1,
          "lines_changed": 9
        },
        {
          "path": "src/Domain/Import/TcxImporter.php",
          "insertions": 29,
          "deletions": 3,
          "lines_changed": 32
        },
        {
          "path": "src/Domain/Import/TrackpointData.php",
          "insertions": 10,
          "deletions": 1,
          "lines_changed": 11
        },
        {
          "path": "src/Domain/Physiology/HeartRateZoneBoundsGenerator.php",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "src/Domain/Physiology/HeartRateZoneConfig.php",
          "insertions": 102,
          "deletions": 0,
          "lines_changed": 102
        },
        {
          "path": "src/Domain/Physiology/HeartRateZoneMethod.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "src/Domain/Physiology/HeartRateZones.php",
          "insertions": 72,
          "deletions": 14,
          "lines_changed": 86
        },
        {
          "path": "src/Domain/Training/GearPhoto.php",
          "insertions": 7,
          "deletions": 4,
          "lines_changed": 11
        },
        {
          "path": "src/Domain/Training/SessionMediaUpload.php",
          "insertions": 32,
          "deletions": 13,
          "lines_changed": 45
        },
        {
          "path": "src/Domain/Training/SessionTagsInput.php",
          "insertions": 96,
          "deletions": 0,
          "lines_changed": 96
        },
        {
          "path": "src/Domain/Training/VideoMetadataStripper.php",
          "insertions": 275,
          "deletions": 0,
          "lines_changed": 275
        },
        {
          "path": "src/Http/Admin/AdminBadgesController.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "src/Http/Admin/AdminQuotesController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/AdminSessionTagsController.php",
          "insertions": 238,
          "deletions": 0,
          "lines_changed": 238
        },
        {
          "path": "src/Http/Admin/AdminSportsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/BadgeDuplicateImage.php",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "src/Http/Admin/InvitationsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/LogsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Calendar/CalendarController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 41,
          "deletions": 12,
          "lines_changed": 53
        },
        {
          "path": "src/Http/Gear/GearAutoAssignController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 52,
          "deletions": 78,
          "lines_changed": 130
        },
        {
          "path": "src/Http/Gear/GearMediaController.php",
          "insertions": 175,
          "deletions": 0,
          "lines_changed": 175
        },
        {
          "path": "src/Http/Gear/GearMediaStore.php",
          "insertions": 150,
          "deletions": 0,
          "lines_changed": 150
        },
        {
          "path": "src/Http/Goals/GoalsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 499,
          "deletions": 68,
          "lines_changed": 567
        },
        {
          "path": "src/Http/Health/InjurySidebarCounter.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "src/Http/Notifications/NotificationsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Physiology/HeartRateZoneConfigFactory.php",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "src/Http/Profile/ProfileController.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Records/BadgesController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Records/RecordsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Sessions/RaceResultStore.php",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 132,
          "deletions": 54,
          "lines_changed": 186
        },
        {
          "path": "src/Http/Sessions/SessionGearStore.php",
          "insertions": 48,
          "deletions": 0,
          "lines_changed": 48
        },
        {
          "path": "src/Http/Sessions/SessionInjuryStore.php",
          "insertions": 56,
          "deletions": 0,
          "lines_changed": 56
        },
        {
          "path": "src/Http/Sessions/SessionMediaController.php",
          "insertions": 49,
          "deletions": 1,
          "lines_changed": 50
        },
        {
          "path": "src/Http/Sessions/SessionMediaStore.php",
          "insertions": 63,
          "deletions": 26,
          "lines_changed": 89
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 264,
          "deletions": 11,
          "lines_changed": 275
        },
        {
          "path": "src/Http/Sessions/SessionTagsStore.php",
          "insertions": 137,
          "deletions": 0,
          "lines_changed": 137
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 39,
          "deletions": 9,
          "lines_changed": 48
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 212,
          "deletions": 19,
          "lines_changed": 231
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Infra/View/Html.php",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "templates/admin_badges.php",
          "insertions": 70,
          "deletions": 1,
          "lines_changed": 71
        },
        {
          "path": "templates/admin_tags.php",
          "insertions": 127,
          "deletions": 0,
          "lines_changed": 127
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 28,
          "deletions": 14,
          "lines_changed": 42
        },
        {
          "path": "templates/gear.php",
          "insertions": 27,
          "deletions": 10,
          "lines_changed": 37
        },
        {
          "path": "templates/gear_detail.php",
          "insertions": 131,
          "deletions": 9,
          "lines_changed": 140
        },
        {
          "path": "templates/goals.php",
          "insertions": 4,
          "deletions": 6,
          "lines_changed": 10
        },
        {
          "path": "templates/health.php",
          "insertions": 170,
          "deletions": 40,
          "lines_changed": 210
        },
        {
          "path": "templates/partials/admin_tabs.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 15,
          "deletions": 1,
          "lines_changed": 16
        },
        {
          "path": "templates/partials/app_topbar.php",
          "insertions": 10,
          "deletions": 1,
          "lines_changed": 11
        },
        {
          "path": "templates/partials/badge_form_fields.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/partials/gear_edit_modal.php",
          "insertions": 2,
          "deletions": 4,
          "lines_changed": 6
        },
        {
          "path": "templates/partials/gear_form_fields.php",
          "insertions": 0,
          "deletions": 20,
          "lines_changed": 20
        },
        {
          "path": "templates/partials/injury_form_fields.php",
          "insertions": 116,
          "deletions": 0,
          "lines_changed": 116
        },
        {
          "path": "templates/partials/tag_edit_modal.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "templates/partials/tag_form_fields.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "templates/profile.php",
          "insertions": 8,
          "deletions": 1,
          "lines_changed": 9
        },
        {
          "path": "templates/races.php",
          "insertions": 4,
          "deletions": 6,
          "lines_changed": 10
        },
        {
          "path": "templates/records.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 273,
          "deletions": 30,
          "lines_changed": 303
        },
        {
          "path": "templates/sessions.php",
          "insertions": 29,
          "deletions": 2,
          "lines_changed": 31
        },
        {
          "path": "templates/settings.php",
          "insertions": 120,
          "deletions": 12,
          "lines_changed": 132
        },
        {
          "path": "tests/Domain/FitImporterTest.php",
          "insertions": 131,
          "deletions": 0,
          "lines_changed": 131
        },
        {
          "path": "tests/Domain/HeartRateZoneBoundsGeneratorTest.php",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "tests/Domain/HeartRateZoneConfigTest.php",
          "insertions": 111,
          "deletions": 0,
          "lines_changed": 111
        },
        {
          "path": "tests/Domain/HeartRateZonesTest.php",
          "insertions": 42,
          "deletions": 0,
          "lines_changed": 42
        },
        {
          "path": "tests/Domain/InjuryTest.php",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "tests/Domain/SessionMediaUploadTest.php",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        },
        {
          "path": "tests/Domain/SessionTagsInputTest.php",
          "insertions": 119,
          "deletions": 0,
          "lines_changed": 119
        },
        {
          "path": "tests/Domain/TcxImporterTest.php",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "tests/Domain/VideoMetadataStripperTest.php",
          "insertions": 118,
          "deletions": 0,
          "lines_changed": 118
        },
        {
          "path": "tests/Http/Admin/BadgeDuplicateImageTest.php",
          "insertions": 54,
          "deletions": 0,
          "lines_changed": 54
        },
        {
          "path": "tests/Http/Physiology/HeartRateZoneConfigFactoryTest.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 281,
          "deletions": 10,
          "lines_changed": 291
        },
        {
          "path": "tests/Infra/View/HtmlTest.php",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "tests/fixtures/sample_cadence.tcx",
          "insertions": 42,
          "deletions": 0,
          "lines_changed": 42
        },
        {
          "path": "tests/fixtures/sample_power.tcx",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        }
      ],
      "files_changed": 105,
      "insertions": 7896,
      "deletions": 514
    },
    {
      "hash": "41a86004dd796aa604e1a5d9e5c0cc63c52e357a",
      "short_hash": "41a8600",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785769355,
      "message": "Stats - Tendances de volume, page /trends (SPOR-315/316/317/318)\n\nNouvelle page dediee au volume d'entrainement (distance + duree) par\nsport : histogramme hebdomadaire ou mensuel (toggle sans rechargement),\navec comparaison \"a date\" a la semaine/mois/annee precedents. Demande\nexplicite de l'utilisateur.\n\n- TrendPeriod (Week|Month, pur) : N buckets consecutifs dans le passe,\n  meme convention \"semaine = lundi\" que GoalPeriod mais classe separee\n  (besoin different : buckets multiples vs periode courante unique).\n- VolumeSeries::bucket() (pur) : repartit des lignes brutes dans des\n  buckets [start,end[.\n- VolumeComparison::toDate() (pur) : compare \"a date\" (meme duree\n  ecoulee des deux cotes) plutot qu'une periode pleine vs partielle\n  (divergence deliberee vs DashboardController::weekData()::loadDelta,\n  documentee).\n- VolumeHistory : meme UNION ALL sessions/session_segments que\n  GoalsController::currentVolume() (segments multisport comptes dans\n  le bon sport), une requete par sport sur toute la fenetre.\n- TrendsController + templates/trends.php : une section par sport du\n  catalogue, filtre client (comme /records), histogramme Distance\n  seulement si PersonalRecords::isSupported(), Duree toujours.\n- L'entree sidebar \"Trends\" existait deja en placeholder v2 depuis la\n  maquette d'origine ; cablee ici (href + retrait du tag v2).\n\nVerifie manuellement de bout en bout (domaine+infra+rendu template)\ncontre un utilisateur reel, y compris un sport personnalise.",
      "message_short": "Stats - Tendances de volume, page /trends (SPOR-315/316/317/318)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "public/assets/js/trends-charts.js",
        "src/Domain/Training/TrendPeriod.php",
        "src/Domain/Training/VolumeComparison.php",
        "src/Domain/Training/VolumeSeries.php",
        "src/Http/Training/VolumeHistory.php",
        "src/Http/Trends/TrendsController.php",
        "templates/partials/app_sidebar.php",
        "templates/trends.php",
        "tests/Domain/TrendPeriodTest.php",
        "tests/Domain/VolumeComparisonTest.php",
        "tests/Domain/VolumeSeriesTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 98,
          "deletions": 0,
          "lines_changed": 98
        },
        {
          "path": "config/routes.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "public/assets/js/trends-charts.js",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "src/Domain/Training/TrendPeriod.php",
          "insertions": 73,
          "deletions": 0,
          "lines_changed": 73
        },
        {
          "path": "src/Domain/Training/VolumeComparison.php",
          "insertions": 84,
          "deletions": 0,
          "lines_changed": 84
        },
        {
          "path": "src/Domain/Training/VolumeSeries.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "src/Http/Training/VolumeHistory.php",
          "insertions": 52,
          "deletions": 0,
          "lines_changed": 52
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 110,
          "deletions": 0,
          "lines_changed": 110
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/trends.php",
          "insertions": 142,
          "deletions": 0,
          "lines_changed": 142
        },
        {
          "path": "tests/Domain/TrendPeriodTest.php",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        },
        {
          "path": "tests/Domain/VolumeComparisonTest.php",
          "insertions": 104,
          "deletions": 0,
          "lines_changed": 104
        },
        {
          "path": "tests/Domain/VolumeSeriesTest.php",
          "insertions": 64,
          "deletions": 0,
          "lines_changed": 64
        }
      ],
      "files_changed": 14,
      "insertions": 922,
      "deletions": 1
    },
    {
      "hash": "d97d1dce94d49a4d3f83d601e9486e92c7a6bf92",
      "short_hash": "d97d1dc",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785767108,
      "message": "Fix(FIT): Import - Graphe Elevation absent sur les seances velo (SPOR-314)\n\nCertains compteurs velo (Garmin Edge notamment) ne renseignent QUE le\nchamp enhanced_altitude (78, uint32), jamais le champ altitude (2,\nuint16) que FitImporter lisait exclusivement — signale par\nl'utilisateur sur /sessions/138, ou le graphe Elevation n'apparaissait\npas du tout. decodeAltitude() essaie desormais le 78 en premier,\nretombe sur le 2 pour les appareils plus anciens (meme formule,\nsentinelle d'invalidite differente selon la taille).\n\nAucun backfill necessaire : l'elevation est toujours recalculee a\nl'affichage depuis le blob FIT brut, jamais persistee.",
      "message_short": "Fix(FIT): Import - Graphe Elevation absent sur les seances velo (SPOR-314)",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "src/Domain/Import/FitImporter.php",
        "tests/Domain/FitImporterTest.php"
      ],
      "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": "src/Domain/Import/FitImporter.php",
          "insertions": 14,
          "deletions": 1,
          "lines_changed": 15
        },
        {
          "path": "tests/Domain/FitImporterTest.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        }
      ],
      "files_changed": 4,
      "insertions": 90,
      "deletions": 1
    },
    {
      "hash": "904f38ea1e69689f1aef8817439dd9ed7aae67d2",
      "short_hash": "904f38e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785766201,
      "message": "Dashboard - Graphes en Chart.js (SPOR-313)\n\nSuite de SPOR-312 : migre les deux derniers graphiques du dashboard\nencore sur le stand-in SVG viz.js (charge des 7 derniers jours,\napercu FC de la derniere seance) vers Chart.js. viz.js n'est plus\ncharge nulle part dans l'application apres cette story (fichier\nconserve tel quel, non supprime). Barres en couleur pleine plutot\nque le degrade CSS --grad-aurora de l'ecran, par coherence avec les\nautres graphes Chart.js du projet qui font le meme choix.",
      "message_short": "Dashboard - Graphes en Chart.js (SPOR-313)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/js/dashboard-charts.js",
        "templates/dashboard.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "public/assets/js/dashboard-charts.js",
          "insertions": 106,
          "deletions": 0,
          "lines_changed": 106
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 14,
          "deletions": 3,
          "lines_changed": 17
        }
      ],
      "files_changed": 4,
      "insertions": 165,
      "deletions": 4
    },
    {
      "hash": "5fa06f12df7e2a2557f4de0d272e78ad9fc9e578",
      "short_hash": "5fa06f1",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785766160,
      "message": "Sessions - Graphes FC/Pace/Elevation en Chart.js (SPOR-312)\n\nRemplace le stand-in SVG viz.js sur l'onglet Summary du detail de\nseance (dernier ecran de la page de detail a en dependre) : la\ngraduation Y/etiquettes X restent rendues en PHP (memes classes CSS\nqu'avant), seule la courbe est desormais tracee par Chart.js sur\ncanvas. Nouveau SessionPresenter::lineChartPayload() (pur) pour le\npayload JSON ; la couleur FC (dependante du sport, y compris un sport\npersonnalise SPOR-277) est resolue cote JS via getComputedStyle() —\nun canvas ne resout pas var() comme le ferait un attribut SVG.",
      "message_short": "Sessions - Graphes FC/Pace/Elevation en Chart.js (SPOR-312)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/css/components.css",
        "public/assets/js/session-charts.js",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionPresenter.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": 63,
          "deletions": 0,
          "lines_changed": 63
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "public/assets/js/session-charts.js",
          "insertions": 84,
          "deletions": 0,
          "lines_changed": 84
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 10,
          "deletions": 6,
          "lines_changed": 16
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 29,
          "deletions": 0,
          "lines_changed": 29
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 62,
          "deletions": 15,
          "lines_changed": 77
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        }
      ],
      "files_changed": 8,
      "insertions": 275,
      "deletions": 22
    },
    {
      "hash": "660ec9e732e1d61949863ce22bcbc3cb730509b9",
      "short_hash": "660ec9e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785760370,
      "message": "Docs + mindmap : synchronise TECHNICAL.md et la mindmap SPOR pour SPOR-271 a 310\n\nTECHNICAL.md documente en detail les 12 commits precedents (import FIT,\nsports/sous-categories ouvertes, dashboard Next race, gear auto-assign,\nliaison course/documents requis, resultats de course/age-grading,\nentree Profil, medias de session, mot de passe renforce/onglet\nAdministratif, records par sous-sport). Mindmap : les noeuds SPOR-271 a\nSPOR-310 passent en Done (🟢), conformement a la regle CLAUDE.md\n(story codee + testee => transition Jira + emoji dans la meme passe).",
      "message_short": "Docs + mindmap : synchronise TECHNICAL.md et la mindmap SPOR pour SPOR-271 a 310",
      "type": "docs",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 63,
          "deletions": 15,
          "lines_changed": 78
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 2949,
          "deletions": 0,
          "lines_changed": 2949
        }
      ],
      "files_changed": 2,
      "insertions": 3012,
      "deletions": 15
    },
    {
      "hash": "06fecc031b6888f4a1a18da37c4351b1beb3f302",
      "short_hash": "06fecc0",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785760344,
      "message": "Cablage JS front pour les features ci-dessus (sports/sous-sports, gear\nauto-assign, medias de session, liaison course, documents administratifs,\nrecords par sous-sport)\n\nFichier partage par toutes les stories SPOR-271 a 310 (modales admin\nsports/sous-sports, cases a cocher gear auto-assign, upload/preview\nmedias de session, modale de liaison seance<->course, formulaires\ndocument medical/PPS, initRecordsSubsportFilters() — meme famille de\nmecanisme que le filtre sport existant mais imbrique un niveau plus bas,\nfonction separee plutot que reutilisation forcee de\ninitGearSubsportFilters() puisque la semantique differe, voir\nTECHNICAL.md) ; pas de decoupage par story, ce fichier n'a pas de\nfrontiere naturelle entre elles.",
      "message_short": "Cablage JS front pour les features ci-dessus (sports/sous-sports, gear",
      "type": "feat",
      "modified_files": [
        "public/assets/js/interactions.js"
      ],
      "modified_files_details": [
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 425,
          "deletions": 5,
          "lines_changed": 430
        }
      ],
      "files_changed": 1,
      "insertions": 425,
      "deletions": 5
    },
    {
      "hash": "0138b5fa41921af6ceaaf70f6c877ed37ba1c8d7",
      "short_hash": "0138b5f",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785760308,
      "message": "Stats - Records personnels segmentes par sous-sport + filtre courses (SPOR-307/308)\n\nPour Run/Bike/Swim (Triathlon n'a pas de sous-sports), le controleur\nprecalcule un bucket \"all\" (comportement historique inchange) plus un\nbucket par sous-sport du catalogue, chacun avec ses propres absolu/\nannee/relatif/progression — un sous-sport donne des records DIFFERENTS\npour un meme palier (le 5K le plus rapide en Trail n'est pas le meme\nqu'en Road), pas un simple filtre visuel comme sur /gear. Filtre\n\"courses uniquement\" en complement (SPOR-308).\n\nComplete au passage : RelativeSplitComputer calcule aussi sur les\nseances FIT (pas seulement TCX) et SessionLoadCalculator lit le\ncoefficient de charge depuis le catalogue de sports (sports\npersonnalises inclus) plutot qu'un switch fige.",
      "message_short": "Stats - Records personnels segmentes par sous-sport + filtre courses (SPOR-307/308)",
      "type": "other",
      "modified_files": [
        "src/Domain/Training/PersonalRecords.php",
        "src/Http/Records/RecordsController.php",
        "src/Http/Records/RelativeSplitComputer.php",
        "src/Http/Sessions/SessionLoadCalculator.php",
        "templates/records.php",
        "tests/Domain/PersonalRecordsTest.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Domain/Training/PersonalRecords.php",
          "insertions": 43,
          "deletions": 7,
          "lines_changed": 50
        },
        {
          "path": "src/Http/Records/RecordsController.php",
          "insertions": 152,
          "deletions": 43,
          "lines_changed": 195
        },
        {
          "path": "src/Http/Records/RelativeSplitComputer.php",
          "insertions": 14,
          "deletions": 10,
          "lines_changed": 24
        },
        {
          "path": "src/Http/Sessions/SessionLoadCalculator.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "templates/records.php",
          "insertions": 74,
          "deletions": 29,
          "lines_changed": 103
        },
        {
          "path": "tests/Domain/PersonalRecordsTest.php",
          "insertions": 68,
          "deletions": 41,
          "lines_changed": 109
        }
      ],
      "files_changed": 6,
      "insertions": 354,
      "deletions": 131
    },
    {
      "hash": "97b268c3be456ec018597492fb473f152f6457f8",
      "short_hash": "97b268c",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785760246,
      "message": "Compte : politique de mot de passe renforcee + onglet Administratif (SPOR-299-301/302-305/309/310)\n\nOnglet Security (Settings) pour changer son mot de passe, separe du\nformulaire de profil, meme throttle et cout constant que le login.\nRegle renforcee (13+ caracteres, majuscule/minuscule/chiffre/caractere\nspecial) partagee entre inscription, reset et changement, avec\nvalidation client-side.\n\nOnglet Administratif : categorie d'age (FFA/FFTri/FFC/FFN/Generique)\naffichee sur une frise avec curseur, certificat medical/PPS/licence\nen upload PDF PAR SPORT (SPOR-309) avec date de validite et alertes\n(notifications certificate_expiring_soon/expired/race_conflict).\nMigration 059 : une course peut requerir PLUSIEURS documents a la\nfois (tableau JSON) depuis que ces documents sont distincts par sport.",
      "message_short": "Compte : politique de mot de passe renforcee + onglet Administratif (SPOR-299-301/302-305/309/310)",
      "type": "other",
      "modified_files": [
        "config/routes.php",
        "db/migrations/054_add_medical_documents.sql",
        "db/migrations/055_add_certificate_notification_types.sql",
        "db/migrations/056_add_medical_document_number.sql",
        "db/migrations/057_add_race_required_document.sql",
        "db/migrations/058_add_sport_to_medical_documents.sql",
        "db/migrations/059_add_race_required_documents.sql",
        "public/assets/css/app.css",
        "src/Domain/Auth/PasswordChangeInput.php",
        "src/Domain/Auth/RegistrationRules.php",
        "src/Domain/Health/AgeCategoryCalculator.php",
        "src/Domain/Health/AgeCategorySystem.php",
        "src/Domain/Health/DocumentExpiry.php",
        "src/Domain/Health/MedicalDocumentUpload.php",
        "src/Domain/Health/SportDocumentTypes.php",
        "src/Http/Auth/PasswordResetController.php",
        "src/Http/Auth/RegisterController.php",
        "src/Http/Notifications/NotificationGenerator.php",
        "src/Http/Settings/AgeCategoryPresenter.php",
        "src/Http/Settings/MedicalDocumentController.php",
        "src/Http/Settings/MedicalDocumentStatus.php",
        "src/Http/Settings/SettingsController.php",
        "templates/emails/password_changed.php",
        "templates/notifications.php",
        "templates/partials/medical_document_card.php",
        "templates/register.php",
        "templates/reset_password.php",
        "templates/settings.php",
        "tests/Domain/AgeCategoryCalculatorTest.php",
        "tests/Domain/DocumentExpiryTest.php",
        "tests/Domain/MedicalDocumentUploadTest.php",
        "tests/Domain/PasswordChangeInputTest.php",
        "tests/Domain/RegistrationRulesTest.php",
        "tests/Domain/SportDocumentTypesTest.php"
      ],
      "modified_files_details": [
        {
          "path": "config/routes.php",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "db/migrations/054_add_medical_documents.sql",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "db/migrations/055_add_certificate_notification_types.sql",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "db/migrations/056_add_medical_document_number.sql",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "db/migrations/057_add_race_required_document.sql",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "db/migrations/058_add_sport_to_medical_documents.sql",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "db/migrations/059_add_race_required_documents.sql",
          "insertions": 22,
          "deletions": 0,
          "lines_changed": 22
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "src/Domain/Auth/PasswordChangeInput.php",
          "insertions": 54,
          "deletions": 0,
          "lines_changed": 54
        },
        {
          "path": "src/Domain/Auth/RegistrationRules.php",
          "insertions": 25,
          "deletions": 2,
          "lines_changed": 27
        },
        {
          "path": "src/Domain/Health/AgeCategoryCalculator.php",
          "insertions": 180,
          "deletions": 0,
          "lines_changed": 180
        },
        {
          "path": "src/Domain/Health/AgeCategorySystem.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "src/Domain/Health/DocumentExpiry.php",
          "insertions": 61,
          "deletions": 0,
          "lines_changed": 61
        },
        {
          "path": "src/Domain/Health/MedicalDocumentUpload.php",
          "insertions": 47,
          "deletions": 0,
          "lines_changed": 47
        },
        {
          "path": "src/Domain/Health/SportDocumentTypes.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "src/Http/Auth/PasswordResetController.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Auth/RegisterController.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Notifications/NotificationGenerator.php",
          "insertions": 68,
          "deletions": 1,
          "lines_changed": 69
        },
        {
          "path": "src/Http/Settings/AgeCategoryPresenter.php",
          "insertions": 76,
          "deletions": 0,
          "lines_changed": 76
        },
        {
          "path": "src/Http/Settings/MedicalDocumentController.php",
          "insertions": 330,
          "deletions": 0,
          "lines_changed": 330
        },
        {
          "path": "src/Http/Settings/MedicalDocumentStatus.php",
          "insertions": 104,
          "deletions": 0,
          "lines_changed": 104
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 136,
          "deletions": 17,
          "lines_changed": 153
        },
        {
          "path": "templates/emails/password_changed.php",
          "insertions": 72,
          "deletions": 0,
          "lines_changed": 72
        },
        {
          "path": "templates/notifications.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "templates/partials/medical_document_card.php",
          "insertions": 142,
          "deletions": 0,
          "lines_changed": 142
        },
        {
          "path": "templates/register.php",
          "insertions": 5,
          "deletions": 2,
          "lines_changed": 7
        },
        {
          "path": "templates/reset_password.php",
          "insertions": 6,
          "deletions": 3,
          "lines_changed": 9
        },
        {
          "path": "templates/settings.php",
          "insertions": 173,
          "deletions": 6,
          "lines_changed": 179
        },
        {
          "path": "tests/Domain/AgeCategoryCalculatorTest.php",
          "insertions": 105,
          "deletions": 0,
          "lines_changed": 105
        },
        {
          "path": "tests/Domain/DocumentExpiryTest.php",
          "insertions": 134,
          "deletions": 0,
          "lines_changed": 134
        },
        {
          "path": "tests/Domain/MedicalDocumentUploadTest.php",
          "insertions": 64,
          "deletions": 0,
          "lines_changed": 64
        },
        {
          "path": "tests/Domain/PasswordChangeInputTest.php",
          "insertions": 93,
          "deletions": 0,
          "lines_changed": 93
        },
        {
          "path": "tests/Domain/RegistrationRulesTest.php",
          "insertions": 8,
          "deletions": 4,
          "lines_changed": 12
        },
        {
          "path": "tests/Domain/SportDocumentTypesTest.php",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        }
      ],
      "files_changed": 34,
      "insertions": 2190,
      "deletions": 38
    },
    {
      "hash": "468d43efc2cb2932d4fe298dc79b84f0ddfe6056",
      "short_hash": "468d43e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785759974,
      "message": "Sessions - Medias de session : photo/video/fichier + badges (SPOR-294-296/298)\n\nAjout de medias (photo/video/PDF) en fin de page de detail de seance\n(galerie 256x256 recadree, action suppression), route dediee\n(SessionMediaController, meme raisonnement que GearAutoAssignController\npour /gear plutot que d'alourdir SessionDetailController). Badge Photo/\nVideo/Fichier sur les listes de seances (dashboard/sessions/detail) via\nSessionPresenter::mediaBadges() — ordre fixe photo/video/other, jamais\nduplique par type.",
      "message_short": "Sessions - Medias de session : photo/video/fichier + badges (SPOR-294-296/298)",
      "type": "other",
      "modified_files": [
        "config/routes.php",
        "db/migrations/053_add_session_media.sql",
        "public/assets/css/app.css",
        "public/assets/css/components.css",
        "public/assets/js/icons.js",
        "src/Domain/Training/SessionMediaUpload.php",
        "src/Http/Sessions/SessionMediaController.php",
        "src/Http/Sessions/SessionMediaStore.php",
        "src/Http/Sessions/SessionPresenter.php",
        "tests/Domain/SessionMediaUploadTest.php",
        "tests/Http/Sessions/SessionPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "config/routes.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "db/migrations/053_add_session_media.sql",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "public/assets/js/icons.js",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "src/Domain/Training/SessionMediaUpload.php",
          "insertions": 92,
          "deletions": 0,
          "lines_changed": 92
        },
        {
          "path": "src/Http/Sessions/SessionMediaController.php",
          "insertions": 172,
          "deletions": 0,
          "lines_changed": 172
        },
        {
          "path": "src/Http/Sessions/SessionMediaStore.php",
          "insertions": 124,
          "deletions": 0,
          "lines_changed": 124
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "tests/Domain/SessionMediaUploadTest.php",
          "insertions": 138,
          "deletions": 0,
          "lines_changed": 138
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        }
      ],
      "files_changed": 11,
      "insertions": 670,
      "deletions": 1
    },
    {
      "hash": "e58c7642b1358cc1041ce2e4c3335d900d06cb92",
      "short_hash": "e58c764",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785759871,
      "message": "Profil - Entree dans le menu de gauche (SPOR-293)\n\nAjoute \"Profile\" a la sidebar (apres Notifications) et met en surbrillance\nl'entree active sur templates/profile.php, qui n'avait jusque-la aucun\nlien de navigation dedie.",
      "message_short": "Profil - Entree dans le menu de gauche (SPOR-293)",
      "type": "other",
      "modified_files": [
        "templates/partials/app_sidebar.php",
        "templates/profile.php"
      ],
      "modified_files_details": [
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "templates/profile.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        }
      ],
      "files_changed": 2,
      "insertions": 2,
      "deletions": 1
    },
    {
      "hash": "54449ca145876513c96082e1f5fb65ba2bb5055a",
      "short_hash": "54449ca",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785759844,
      "message": "Sessions - Resultat de course, age-grading, projection (SPOR-288-292)\n\nOnglet dedie sur le detail de seance pour saisir le resultat officiel\nd'une course (DNS/DNF/temps officiel/classement), avec pourcentage\nage-grading approximatif (style WMA/Alan Jones) et ecart colore vs\nObjectif et vs Prediction (RaceProjectionGap, deja utilise par la carte\ndashboard \"Next race\"). Filtre \"Race only\" cote serveur sur /sessions\n(SPOR-289) avec sa propre puce visuelle.\n\nMigration 045 : lien seance<->course (sous-jacent au lien SPOR-278 et\naux resultats ci-dessus, les deux features partagent la meme colonne).",
      "message_short": "Sessions - Resultat de course, age-grading, projection (SPOR-288-292)",
      "type": "other",
      "modified_files": [
        "db/migrations/045_add_race_session_link.sql",
        "db/migrations/051_add_session_race_results.sql",
        "db/migrations/052_add_race_result_totals.sql",
        "public/assets/css/components.css",
        "src/Domain/Training/AgeGrading.php",
        "src/Domain/Training/RaceProjectionGap.php",
        "src/Domain/Training/RaceResultInput.php",
        "src/Domain/Training/RaceResultVariation.php",
        "src/Http/Sessions/AgeGradePresenter.php",
        "src/Http/Sessions/RaceComparisonPresenter.php",
        "src/Http/Sessions/RaceResultStore.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Training/RaceProjection.php",
        "templates/session_detail.php",
        "tests/Domain/AgeGradingTest.php",
        "tests/Domain/RaceProjectionGapTest.php",
        "tests/Domain/RaceResultInputTest.php",
        "tests/Domain/RaceResultVariationTest.php"
      ],
      "modified_files_details": [
        {
          "path": "db/migrations/045_add_race_session_link.sql",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "db/migrations/051_add_session_race_results.sql",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "db/migrations/052_add_race_result_totals.sql",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "src/Domain/Training/AgeGrading.php",
          "insertions": 132,
          "deletions": 0,
          "lines_changed": 132
        },
        {
          "path": "src/Domain/Training/RaceProjectionGap.php",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "src/Domain/Training/RaceResultInput.php",
          "insertions": 141,
          "deletions": 0,
          "lines_changed": 141
        },
        {
          "path": "src/Domain/Training/RaceResultVariation.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "src/Http/Sessions/AgeGradePresenter.php",
          "insertions": 68,
          "deletions": 0,
          "lines_changed": 68
        },
        {
          "path": "src/Http/Sessions/RaceComparisonPresenter.php",
          "insertions": 100,
          "deletions": 0,
          "lines_changed": 100
        },
        {
          "path": "src/Http/Sessions/RaceResultStore.php",
          "insertions": 162,
          "deletions": 0,
          "lines_changed": 162
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 171,
          "deletions": 14,
          "lines_changed": 185
        },
        {
          "path": "src/Http/Training/RaceProjection.php",
          "insertions": 114,
          "deletions": 0,
          "lines_changed": 114
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 306,
          "deletions": 3,
          "lines_changed": 309
        },
        {
          "path": "tests/Domain/AgeGradingTest.php",
          "insertions": 119,
          "deletions": 0,
          "lines_changed": 119
        },
        {
          "path": "tests/Domain/RaceProjectionGapTest.php",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "tests/Domain/RaceResultInputTest.php",
          "insertions": 252,
          "deletions": 0,
          "lines_changed": 252
        },
        {
          "path": "tests/Domain/RaceResultVariationTest.php",
          "insertions": 68,
          "deletions": 0,
          "lines_changed": 68
        }
      ],
      "files_changed": 18,
      "insertions": 1801,
      "deletions": 17
    },
    {
      "hash": "c205db40121e0ac03dbfa3fd8a971ec5b9de66ba",
      "short_hash": "c205db4",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785759748,
      "message": "Races : lier une seance + document requis pour valider (SPOR-278/306/310)\n\nUne Course peut desormais etre liee a une seance existante/importee\n(modale dediee, lien/delien) plutot que de dependre uniquement de la\ndetection automatique. Statut de validite affiche a cote de la course\n(valide/invalide) base sur le document requis pour le sport concerne\n(certificat medical/PPS), lui-meme calcule automatiquement selon le\nsport avec possibilite de surcharge manuelle (SPOR-310).",
      "message_short": "Races : lier une seance + document requis pour valider (SPOR-278/306/310)",
      "type": "docs",
      "modified_files": [
        "config/routes.php",
        "src/Http/Goals/GoalFormHelpers.php",
        "src/Http/Goals/RacesController.php",
        "templates/partials/goal_form_fields.php",
        "templates/partials/race_link_modal.php",
        "templates/partials/race_link_status.php"
      ],
      "modified_files_details": [
        {
          "path": "config/routes.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "src/Http/Goals/GoalFormHelpers.php",
          "insertions": 25,
          "deletions": 73,
          "lines_changed": 98
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 317,
          "deletions": 14,
          "lines_changed": 331
        },
        {
          "path": "templates/partials/goal_form_fields.php",
          "insertions": 25,
          "deletions": 2,
          "lines_changed": 27
        },
        {
          "path": "templates/partials/race_link_modal.php",
          "insertions": 63,
          "deletions": 0,
          "lines_changed": 63
        },
        {
          "path": "templates/partials/race_link_status.php",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        }
      ],
      "files_changed": 6,
      "insertions": 465,
      "deletions": 90
    },
    {
      "hash": "b896160ee860364263a1e30ffb9a30ebca9a827a",
      "short_hash": "b896160",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785759693,
      "message": "Materiel - Assignation automatique aux seances (SPOR-285/286/287)\n\nCase a cocher \"Auto-assign to matching sessions\" sur un materiel\n(matching sport + sous-sport optionnel) ; les formulaires de saisie/\nedition de seance pre-cochent le materiel eligible avec un\navertissement explicite (jamais silencieux) ; revue retroactive\ndediee (/gear/auto-assign/review) pour appliquer la regle aux seances\ndeja existantes sans tout re-scanner a chaque sauvegarde.",
      "message_short": "Materiel - Assignation automatique aux seances (SPOR-285/286/287)",
      "type": "other",
      "modified_files": [
        "config/routes.php",
        "db/migrations/050_add_gear_auto_assign.sql",
        "src/Domain/Training/GearAutoAssignMatcher.php",
        "src/Http/Gear/GearAutoAssignController.php",
        "src/Http/Gear/GearAutoAssignMap.php",
        "templates/gear_auto_assign_review.php",
        "tests/Domain/GearAutoAssignMatcherTest.php"
      ],
      "modified_files_details": [
        {
          "path": "config/routes.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "db/migrations/050_add_gear_auto_assign.sql",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "src/Domain/Training/GearAutoAssignMatcher.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "src/Http/Gear/GearAutoAssignController.php",
          "insertions": 286,
          "deletions": 0,
          "lines_changed": 286
        },
        {
          "path": "src/Http/Gear/GearAutoAssignMap.php",
          "insertions": 91,
          "deletions": 0,
          "lines_changed": 91
        },
        {
          "path": "templates/gear_auto_assign_review.php",
          "insertions": 140,
          "deletions": 0,
          "lines_changed": 140
        },
        {
          "path": "tests/Domain/GearAutoAssignMatcherTest.php",
          "insertions": 49,
          "deletions": 0,
          "lines_changed": 49
        }
      ],
      "files_changed": 7,
      "insertions": 629,
      "deletions": 0
    },
    {
      "hash": "78ad5194d132a777d7f545578ccd77fcadc9b030",
      "short_hash": "78ad519",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785759340,
      "message": "Dashboard - Carte \"Next race\" avec prediction coloree (SPOR-274/275/276)\n\nCarte dediee a la prochaine course a venir entre le Reve et le recap\n\"This week\" : temps/objectif vises avec prediction coloree (vert/bleu/\norange) issue de RaceProjectionGap. templates/races.php reprend le\nmeme code couleur/icones pour rester coherent avec le dashboard.\nGoalCountdown::label() etend le format court a un plus grand nombre\nde cas (jours multiples/1 jour/aujourd'hui).",
      "message_short": "Dashboard - Carte \"Next race\" avec prediction coloree (SPOR-274/275/276)",
      "type": "other",
      "modified_files": [
        "src/Domain/Training/GoalCountdown.php",
        "src/Http/Dashboard/DashboardController.php",
        "templates/dashboard.php",
        "templates/races.php",
        "tests/Domain/GoalCountdownTest.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Domain/Training/GoalCountdown.php",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 83,
          "deletions": 2,
          "lines_changed": 85
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 73,
          "deletions": 5,
          "lines_changed": 78
        },
        {
          "path": "templates/races.php",
          "insertions": 38,
          "deletions": 7,
          "lines_changed": 45
        },
        {
          "path": "tests/Domain/GoalCountdownTest.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        }
      ],
      "files_changed": 5,
      "insertions": 247,
      "deletions": 14
    },
    {
      "hash": "c47d0e7fd7cc9c1518cbeaef3df67ec44e11bd47",
      "short_hash": "c47d0e7",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785759317,
      "message": "Sports & sous-categories : catalogue ouvert, admin, cablage global (SPOR-272/273/277/279-284)\n\nSport devient une classe valeur ouverte (plus un enum ferme) : les 8\nsports d'origine restent des constantes non supprimables (coefficients\nde charge actes, CLAUDE.md), mais un admin peut desormais ajouter/\nediter/supprimer des sports et sous-sports personnalises (table\nsports/subsports en base, SportCatalog/SubsportCatalog). ViewRenderer\ninjecte le catalogue (icone/couleur/coefficient) dans chaque vue sans\ntoucher aux ~20 controleurs qui l'utilisaient deja.\n\nSous-sport cable dans les formulaires de seance/materiel, le filtre\n/gear, les badges (scope optionnel par sous-sport, SPOR-284) et\nl'onboarding (reste volontairement limite aux 8 sports d'origine).\n\nMigrations 043/044 (catalogue sports ouvert), 046-049 (sous-sports +\ncablage seance/materiel/badge).",
      "message_short": "Sports & sous-categories : catalogue ouvert, admin, cablage global (SPOR-272/273/277/279-284)",
      "type": "other",
      "modified_files": [
        "config/routes.php",
        "db/migrations/043_add_sports_table.sql",
        "db/migrations/044_sports_open_catalog.sql",
        "db/migrations/046_add_subsports.sql",
        "db/migrations/047_add_session_subsport.sql",
        "db/migrations/048_add_gear_subsports.sql",
        "db/migrations/049_add_badge_subsport.sql",
        "public/index.php",
        "src/Domain/Training/BadgeInput.php",
        "src/Domain/Training/DistanceMilestoneInput.php",
        "src/Domain/Training/GearInput.php",
        "src/Domain/Training/GearSuggestion.php",
        "src/Domain/Training/SessionInput.php",
        "src/Domain/Training/Sport.php",
        "src/Http/Admin/AdminBadgesController.php",
        "src/Http/Admin/AdminSportsController.php",
        "src/Http/Auth/OnboardingController.php",
        "src/Http/Gear/GearController.php",
        "src/Http/Gear/GearSuggestions.php",
        "src/Http/Records/BadgeCatalog.php",
        "src/Http/Records/BadgesController.php",
        "src/Http/Records/BadgesSidebarCounter.php",
        "src/Http/Records/SessionRecordBadge.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Infra/Training/SportCatalog.php",
        "src/Infra/Training/SubsportCatalog.php",
        "src/Infra/View/ViewRenderer.php",
        "templates/admin_badges.php",
        "templates/admin_sports.php",
        "templates/badges.php",
        "templates/gear.php",
        "templates/gear_detail.php",
        "templates/onboarding.php",
        "templates/partials/admin_tabs.php",
        "templates/partials/app_foot.php",
        "templates/partials/badge_form_fields.php",
        "templates/partials/gear_form_fields.php",
        "templates/partials/session_modals.php",
        "templates/partials/sport_delete_modals.php",
        "templates/partials/sport_edit_modal.php",
        "templates/partials/sport_form_fields.php",
        "templates/partials/subsport_delete_modals.php",
        "templates/partials/subsport_edit_modal.php",
        "templates/partials/subsport_form_fields.php",
        "templates/sessions.php",
        "tests/Domain/BadgeInputTest.php",
        "tests/Domain/DistanceMilestoneInputTest.php",
        "tests/Domain/GearInputTest.php",
        "tests/Domain/GearSuggestionTest.php",
        "tests/Domain/SessionInputTest.php",
        "tests/Domain/SportTest.php",
        "tests/Domain/TrainingLoadTest.php",
        "tests/Http/Records/BadgeCatalogTest.php"
      ],
      "modified_files_details": [
        {
          "path": "config/routes.php",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "db/migrations/043_add_sports_table.sql",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "db/migrations/044_sports_open_catalog.sql",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "db/migrations/046_add_subsports.sql",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "db/migrations/047_add_session_subsport.sql",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "db/migrations/048_add_gear_subsports.sql",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "db/migrations/049_add_badge_subsport.sql",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "public/index.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "src/Domain/Training/BadgeInput.php",
          "insertions": 48,
          "deletions": 5,
          "lines_changed": 53
        },
        {
          "path": "src/Domain/Training/DistanceMilestoneInput.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Domain/Training/GearInput.php",
          "insertions": 41,
          "deletions": 7,
          "lines_changed": 48
        },
        {
          "path": "src/Domain/Training/GearSuggestion.php",
          "insertions": 21,
          "deletions": 4,
          "lines_changed": 25
        },
        {
          "path": "src/Domain/Training/SessionInput.php",
          "insertions": 24,
          "deletions": 2,
          "lines_changed": 26
        },
        {
          "path": "src/Domain/Training/Sport.php",
          "insertions": 71,
          "deletions": 24,
          "lines_changed": 95
        },
        {
          "path": "src/Http/Admin/AdminBadgesController.php",
          "insertions": 41,
          "deletions": 10,
          "lines_changed": 51
        },
        {
          "path": "src/Http/Admin/AdminSportsController.php",
          "insertions": 674,
          "deletions": 0,
          "lines_changed": 674
        },
        {
          "path": "src/Http/Auth/OnboardingController.php",
          "insertions": 6,
          "deletions": 1,
          "lines_changed": 7
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 121,
          "deletions": 15,
          "lines_changed": 136
        },
        {
          "path": "src/Http/Gear/GearSuggestions.php",
          "insertions": 94,
          "deletions": 20,
          "lines_changed": 114
        },
        {
          "path": "src/Http/Records/BadgeCatalog.php",
          "insertions": 51,
          "deletions": 26,
          "lines_changed": 77
        },
        {
          "path": "src/Http/Records/BadgesController.php",
          "insertions": 40,
          "deletions": 17,
          "lines_changed": 57
        },
        {
          "path": "src/Http/Records/BadgesSidebarCounter.php",
          "insertions": 5,
          "deletions": 3,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Records/SessionRecordBadge.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 167,
          "deletions": 26,
          "lines_changed": 193
        },
        {
          "path": "src/Infra/Training/SportCatalog.php",
          "insertions": 62,
          "deletions": 0,
          "lines_changed": 62
        },
        {
          "path": "src/Infra/Training/SubsportCatalog.php",
          "insertions": 57,
          "deletions": 0,
          "lines_changed": 57
        },
        {
          "path": "src/Infra/View/ViewRenderer.php",
          "insertions": 15,
          "deletions": 1,
          "lines_changed": 16
        },
        {
          "path": "templates/admin_badges.php",
          "insertions": 19,
          "deletions": 3,
          "lines_changed": 22
        },
        {
          "path": "templates/admin_sports.php",
          "insertions": 210,
          "deletions": 0,
          "lines_changed": 210
        },
        {
          "path": "templates/badges.php",
          "insertions": 10,
          "deletions": 2,
          "lines_changed": 12
        },
        {
          "path": "templates/gear.php",
          "insertions": 46,
          "deletions": 2,
          "lines_changed": 48
        },
        {
          "path": "templates/gear_detail.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "templates/onboarding.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/partials/admin_tabs.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "templates/partials/app_foot.php",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "templates/partials/badge_form_fields.php",
          "insertions": 30,
          "deletions": 1,
          "lines_changed": 31
        },
        {
          "path": "templates/partials/gear_form_fields.php",
          "insertions": 28,
          "deletions": 2,
          "lines_changed": 30
        },
        {
          "path": "templates/partials/session_modals.php",
          "insertions": 57,
          "deletions": 19,
          "lines_changed": 76
        },
        {
          "path": "templates/partials/sport_delete_modals.php",
          "insertions": 66,
          "deletions": 0,
          "lines_changed": 66
        },
        {
          "path": "templates/partials/sport_edit_modal.php",
          "insertions": 55,
          "deletions": 0,
          "lines_changed": 55
        },
        {
          "path": "templates/partials/sport_form_fields.php",
          "insertions": 56,
          "deletions": 0,
          "lines_changed": 56
        },
        {
          "path": "templates/partials/subsport_delete_modals.php",
          "insertions": 64,
          "deletions": 0,
          "lines_changed": 64
        },
        {
          "path": "templates/partials/subsport_edit_modal.php",
          "insertions": 62,
          "deletions": 0,
          "lines_changed": 62
        },
        {
          "path": "templates/partials/subsport_form_fields.php",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "templates/sessions.php",
          "insertions": 101,
          "deletions": 18,
          "lines_changed": 119
        },
        {
          "path": "tests/Domain/BadgeInputTest.php",
          "insertions": 73,
          "deletions": 0,
          "lines_changed": 73
        },
        {
          "path": "tests/Domain/DistanceMilestoneInputTest.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "tests/Domain/GearInputTest.php",
          "insertions": 62,
          "deletions": 1,
          "lines_changed": 63
        },
        {
          "path": "tests/Domain/GearSuggestionTest.php",
          "insertions": 58,
          "deletions": 0,
          "lines_changed": 58
        },
        {
          "path": "tests/Domain/SessionInputTest.php",
          "insertions": 69,
          "deletions": 3,
          "lines_changed": 72
        },
        {
          "path": "tests/Domain/SportTest.php",
          "insertions": 75,
          "deletions": 16,
          "lines_changed": 91
        },
        {
          "path": "tests/Domain/TrainingLoadTest.php",
          "insertions": 13,
          "deletions": 13,
          "lines_changed": 26
        },
        {
          "path": "tests/Http/Records/BadgeCatalogTest.php",
          "insertions": 125,
          "deletions": 0,
          "lines_changed": 125
        }
      ],
      "files_changed": 53,
      "insertions": 3059,
      "deletions": 248
    },
    {
      "hash": "be681314634d9bb858b94f7c43a72110dd015838",
      "short_hash": "be68131",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785759233,
      "message": "Import FIT (Garmin/ANT+) : parser binaire maison, multisport inclus (SPOR-271)\n\nNouveau FitImporter (format binaire Garmin, sans dependance tierce) et\nMultisportSessionBuilder pour decomposer une activite FIT multisport en\nsegments, sur le meme modele que le TCX multisport existant. SessionData\ngagne un champ optionnel $subSport (indice seul, jamais autoritaire) que\nseul FitImporter renseigne — TCX/GPX n'ont pas cette notion dans leur\nschema (prepare l'epic Sous-categories, SPOR-279+).\n\nJeu de donnees anonymise (GPS translate en Laponie, UnitId a 0) ajoute\npour tester l'import d'une activite d'aviron.",
      "message_short": "Import FIT (Garmin/ANT+) : parser binaire maison, multisport inclus (SPOR-271)",
      "type": "other",
      "modified_files": [
        "References/dataset/rameur-13895717.tcx",
        "src/Domain/Import/FitImporter.php",
        "src/Domain/Import/GpxImporter.php",
        "src/Domain/Import/MultisportSessionBuilder.php",
        "src/Domain/Import/SessionData.php",
        "src/Domain/Import/TcxImporter.php",
        "src/Infra/Import/TrackpointReader.php",
        "tests/Domain/FitImporterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/dataset/rameur-13895717.tcx",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Domain/Import/FitImporter.php",
          "insertions": 538,
          "deletions": 0,
          "lines_changed": 538
        },
        {
          "path": "src/Domain/Import/GpxImporter.php",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "src/Domain/Import/MultisportSessionBuilder.php",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        },
        {
          "path": "src/Domain/Import/SessionData.php",
          "insertions": 8,
          "deletions": 1,
          "lines_changed": 9
        },
        {
          "path": "src/Domain/Import/TcxImporter.php",
          "insertions": 7,
          "deletions": 52,
          "lines_changed": 59
        },
        {
          "path": "src/Infra/Import/TrackpointReader.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "tests/Domain/FitImporterTest.php",
          "insertions": 492,
          "deletions": 0,
          "lines_changed": 492
        }
      ],
      "files_changed": 8,
      "insertions": 1123,
      "deletions": 53
    },
    {
      "hash": "ce4665923503f7a883713f2483ddcd618a2a0eb5",
      "short_hash": "ce46659",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785600453,
      "message": "Materiel : achat (lieu/date), suggestion de rotation, graphique d'utilisation (SPOR-268/269/270)\n\nReprise de trois points de la mindmap mis de cote comme Could/Won't :\nprix/lieu/date d'achat sur la fiche produit, suggestion de rotation\n(anti-repetition + km cumule + tags, affichee sur /gear et en saisie\nmanuelle de seance) et graphique d'utilisation cumule/par seance sur la\nfiche detail (Chart.js).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Materiel : achat (lieu/date), suggestion de rotation, graphique d'utilisation (SPOR-268/269/270)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/042_add_gear_purchase.sql",
        "public/assets/js/gear-usage-chart.js",
        "public/assets/js/interactions.js",
        "src/Domain/Training/GearInput.php",
        "src/Domain/Training/GearSuggestion.php",
        "src/Domain/Training/GearUsageTimeline.php",
        "src/Http/Gear/GearController.php",
        "src/Http/Gear/GearSuggestions.php",
        "src/Http/Sessions/SessionsController.php",
        "templates/gear.php",
        "templates/gear_detail.php",
        "templates/partials/gear_form_fields.php",
        "templates/partials/session_modals.php",
        "tests/Domain/GearInputTest.php",
        "tests/Domain/GearSuggestionTest.php",
        "tests/Domain/GearUsageTimelineTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 107,
          "deletions": 0,
          "lines_changed": 107
        },
        {
          "path": "db/migrations/042_add_gear_purchase.sql",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "public/assets/js/gear-usage-chart.js",
          "insertions": 67,
          "deletions": 0,
          "lines_changed": 67
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "src/Domain/Training/GearInput.php",
          "insertions": 44,
          "deletions": 2,
          "lines_changed": 46
        },
        {
          "path": "src/Domain/Training/GearSuggestion.php",
          "insertions": 74,
          "deletions": 0,
          "lines_changed": 74
        },
        {
          "path": "src/Domain/Training/GearUsageTimeline.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 84,
          "deletions": 12,
          "lines_changed": 96
        },
        {
          "path": "src/Http/Gear/GearSuggestions.php",
          "insertions": 135,
          "deletions": 0,
          "lines_changed": 135
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "templates/gear.php",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "templates/gear_detail.php",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "templates/partials/gear_form_fields.php",
          "insertions": 5,
          "deletions": 2,
          "lines_changed": 7
        },
        {
          "path": "templates/partials/session_modals.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "tests/Domain/GearInputTest.php",
          "insertions": 62,
          "deletions": 0,
          "lines_changed": 62
        },
        {
          "path": "tests/Domain/GearSuggestionTest.php",
          "insertions": 102,
          "deletions": 0,
          "lines_changed": 102
        },
        {
          "path": "tests/Domain/GearUsageTimelineTest.php",
          "insertions": 68,
          "deletions": 0,
          "lines_changed": 68
        }
      ],
      "files_changed": 18,
      "insertions": 910,
      "deletions": 16
    },
    {
      "hash": "c698dbc2636e5171d962a2e20e891dc525c5b490",
      "short_hash": "c698dbc",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785540158,
      "message": "Stats : ACWR, alerte de pic de charge et graphique de tendance (SPOR-267)\n\nComplete la carte \"Training load trend\" avec les 3 elements laisses hors\nperimetre par SPOR-266, tous redemandes explicitement :\n\n- ACWR (ratio aigu:chronique = fatigue/fitness) : sans unite, donc les\n  bandes d'interpretation standard (< 0.8 sous-entraine, 0.8-1.3 optimal,\n  1.3-1.5 prudence, > 1.5 risque eleve) restent valides malgre l'echelle\n  de Charge propre a ce projet - contrairement a un seuil absolu sur\n  \"form\" qui aurait ete arbitraire.\n- Notification quotidienne (NotificationGenerator) quand le ratio entre\n  en zone \"danger\", meme idempotence que les rappels recurrents.\n- Graphique Chart.js (Fitness/Fatigue sur 30 jours) sous les 4 tuiles.\n\nHttp\\Training\\DailyLoadHistory extrait la requete d'agregation quotidienne\npartagee entre le Dashboard et le generateur de notifications.\n\nMigration 041 : \"training_load_high\" ajoute a l'ENUM notifications.source_type\n(trouve et corrige via un test d'integration isole en transaction, rollback\navant commit - source_type est un ENUM strict, pas juste un VARCHAR).\n\nStory SPOR-267 transitionnee vers Done, mindmap a jour.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Stats : ACWR, alerte de pic de charge et graphique de tendance (SPOR-267)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/041_add_training_load_notification.sql",
        "public/assets/js/load-trend-chart.js",
        "src/Domain/Training/TrainingLoadTrend.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Notifications/NotificationGenerator.php",
        "src/Http/Training/DailyLoadHistory.php",
        "templates/dashboard.php",
        "templates/notifications.php",
        "tests/Domain/TrainingLoadTrendTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 82,
          "deletions": 0,
          "lines_changed": 82
        },
        {
          "path": "db/migrations/041_add_training_load_notification.sql",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "public/assets/js/load-trend-chart.js",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "src/Domain/Training/TrainingLoadTrend.php",
          "insertions": 57,
          "deletions": 3,
          "lines_changed": 60
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 23,
          "deletions": 20,
          "lines_changed": 43
        },
        {
          "path": "src/Http/Notifications/NotificationGenerator.php",
          "insertions": 43,
          "deletions": 0,
          "lines_changed": 43
        },
        {
          "path": "src/Http/Training/DailyLoadHistory.php",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 42,
          "deletions": 8,
          "lines_changed": 50
        },
        {
          "path": "templates/notifications.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "tests/Domain/TrainingLoadTrendTest.php",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        }
      ],
      "files_changed": 11,
      "insertions": 436,
      "deletions": 32
    },
    {
      "hash": "e13c8f989d30e7626db60f03f00e83a0fa6790f0",
      "short_hash": "e13c8f9",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785538758,
      "message": "Stats : tendance Fitness/Fatigue/Form basee sur la Charge (SPOR-265/266)\n\nAjoute une carte \"Training load trend\" au dashboard, equivalent\nCTL/ATL/TSB de la mindmap mais base sur la Charge deja calculee par\nseance (pas un vrai TSS, explicitement Could/pas MVP dans CLAUDE.md) :\n\n- Domain\\Training\\TrainingLoadTrend::compute() : moyennes glissantes\n  42j (fitness) / 7j (fatigue) + leur ecart (form), pur et teste.\n- DashboardController agrege la charge quotidienne (GROUP BY DATE)\n  sur les 42 derniers jours et delegue au domaine.\n- Pas de seuils d'alerte ni de graphique : hors perimetre pour cette\n  premiere story (echelle de Charge differente de celle du TSS de la\n  mindmap, un seuil invente serait arbitraire).\n\nEpic SPOR-265 et story SPOR-266 transitionnes vers Done, mindmap a jour.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Stats : tendance Fitness/Fatigue/Form basee sur la Charge (SPOR-265/266)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "src/Domain/Training/TrainingLoadTrend.php",
        "src/Http/Dashboard/DashboardController.php",
        "templates/dashboard.php",
        "tests/Domain/TrainingLoadTrendTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 61,
          "deletions": 0,
          "lines_changed": 61
        },
        {
          "path": "src/Domain/Training/TrainingLoadTrend.php",
          "insertions": 55,
          "deletions": 0,
          "lines_changed": 55
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "tests/Domain/TrainingLoadTrendTest.php",
          "insertions": 74,
          "deletions": 0,
          "lines_changed": 74
        }
      ],
      "files_changed": 6,
      "insertions": 250,
      "deletions": 0
    },
    {
      "hash": "7ee7d80e47eb0106dcca1e1980c585d5481c158e",
      "short_hash": "7ee7d80",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785537506,
      "message": "Citations : table quotes en base + admin (SPOR-262/263/264)\n\nRemplace la liste PHP en dur de DashboardController (rotation jour de\nl'annee) par une table quotes administrable, comme annonce par le\ncommentaire du controleur (\"en attendant la table quotes (V2.4)\") :\n\n- Domain\\Content\\QuoteOfTheDay::pick() extrait la rotation (pure, testee).\n- Migration 040_add_quotes_table.sql, seedee avec les 7 citations et leur\n  ordre d'origine (aucun effet visible sur la citation du jour).\n- AdminQuotesController + onglet admin Quotes, meme squelette que\n  AdminBadgesController (SPOR-243).\n- Dashboard masque la carte \"Citation du jour\" si la table est vide.\n\nEpic SPOR-262 et stories SPOR-263/264 transitionnes vers Done, mindmap a\njour. Vote/visibilite/partage/communaute restent hors perimetre (Could/\nWon't tant que non redemandes).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Citations : table quotes en base + admin (SPOR-262/263/264)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/040_add_quotes_table.sql",
        "src/Domain/Content/QuoteOfTheDay.php",
        "src/Http/Admin/AdminQuotesController.php",
        "src/Http/Dashboard/DashboardController.php",
        "templates/admin_quotes.php",
        "templates/dashboard.php",
        "templates/partials/admin_tabs.php",
        "templates/partials/quote_edit_modal.php",
        "templates/partials/quote_form_fields.php",
        "tests/Domain/QuoteOfTheDayTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 84,
          "deletions": 0,
          "lines_changed": 84
        },
        {
          "path": "config/routes.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "db/migrations/040_add_quotes_table.sql",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "src/Domain/Content/QuoteOfTheDay.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "src/Http/Admin/AdminQuotesController.php",
          "insertions": 261,
          "deletions": 0,
          "lines_changed": 261
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 9,
          "deletions": 15,
          "lines_changed": 24
        },
        {
          "path": "templates/admin_quotes.php",
          "insertions": 126,
          "deletions": 0,
          "lines_changed": 126
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "templates/partials/admin_tabs.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "templates/partials/quote_edit_modal.php",
          "insertions": 68,
          "deletions": 0,
          "lines_changed": 68
        },
        {
          "path": "templates/partials/quote_form_fields.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "tests/Domain/QuoteOfTheDayTest.php",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        }
      ],
      "files_changed": 13,
      "insertions": 734,
      "deletions": 17
    },
    {
      "hash": "7613cc9e97b15196a5b9c19831230cfe9e76b6cd",
      "short_hash": "7613cc9",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1785536013,
      "message": "Notifications, Badges, Races, Gear tags/product/detail, Reminder edit/delete\n\nCloture le lot en attente couvrant SPOR-231 a SPOR-261 : renommage\nAlertes -> Notifications (archivage, heure des rappels Daily), systeme de\nBadges (types Distance/Time/Count, fenetre temporelle, admin, glow),\npage Races dediee separee du cap Objectifs Volume, fiche detail Gear\n(produit, tags, records), edition/suppression d'un rappel Sante, et\nonglets sur la modale d'edition de seance.\n\nToutes les stories concernees sont deja Done cote Jira (mindmap a jour) ;\nSPOR-234/235/177 transitionnees vers Done dans cette passe pour rattraper\nle retard de synchro introduit par la regle CLAUDE.md recemment ajoutee.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "Notifications, Badges, Races, Gear tags/product/detail, Reminder edit/delete",
      "type": "other",
      "modified_files": [
        "CLAUDE.md",
        "References/design/assets/css/components.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "compose.yaml",
        "config/routes.php",
        "config/settings.php",
        "db/migrations/029_add_reminder_alert_time.sql",
        "db/migrations/030_add_alerts_archiving.sql",
        "db/migrations/031_rename_alerts_to_notifications.sql",
        "db/migrations/032_add_badges_table.sql",
        "db/migrations/033_add_goal_performance_type.sql",
        "db/migrations/034_add_goal_performance_target_date.sql",
        "db/migrations/035_add_badge_type.sql",
        "db/migrations/036_add_badge_window.sql",
        "db/migrations/037_add_badge_ignores.sql",
        "db/migrations/038_add_gear_product_fields.sql",
        "db/migrations/039_add_gear_tags.sql",
        "public/assets/css/app.css",
        "public/assets/css/components.css",
        "public/assets/img/badges/placeholder.svg",
        "public/assets/js/alerts.js",
        "public/assets/js/notifications.js",
        "public/assets/js/viz.js",
        "src/Domain/Health/Reminder.php",
        "src/Domain/Training/BadgeImage.php",
        "src/Domain/Training/BadgeInput.php",
        "src/Domain/Training/CumulativeBadges.php",
        "src/Domain/Training/DistanceBadges.php",
        "src/Domain/Training/GearInput.php",
        "src/Domain/Training/GearTagsInput.php",
        "src/Http/Admin/AdminBadgesController.php",
        "src/Http/Admin/InvitationsController.php",
        "src/Http/Admin/LogsController.php",
        "src/Http/Alerts/AlertGenerator.php",
        "src/Http/Alerts/AlertsController.php",
        "src/Http/Alerts/AlertsSidebarCounter.php",
        "src/Http/Calendar/CalendarController.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Gear/GearController.php",
        "src/Http/Goals/GoalFormHelpers.php",
        "src/Http/Goals/GoalsController.php",
        "src/Http/Goals/GoalsSidebarCounter.php",
        "src/Http/Goals/RacesController.php",
        "src/Http/Goals/RacesSidebarCounter.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Notifications/NotificationGenerator.php",
        "src/Http/Notifications/NotificationsController.php",
        "src/Http/Notifications/NotificationsSidebarCounter.php",
        "src/Http/Profile/ProfileController.php",
        "src/Http/Records/BadgeCatalog.php",
        "src/Http/Records/BadgesController.php",
        "src/Http/Records/BadgesSidebarCounter.php",
        "src/Http/Records/RecordsController.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionPresenter.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Http/Settings/SettingsController.php",
        "templates/admin_badges.php",
        "templates/alerts.php",
        "templates/badges.php",
        "templates/gear.php",
        "templates/gear_detail.php",
        "templates/goals.php",
        "templates/health.php",
        "templates/notifications.php",
        "templates/partials/admin_tabs.php",
        "templates/partials/app_sidebar.php",
        "templates/partials/app_topbar.php",
        "templates/partials/badge_edit_modal.php",
        "templates/partials/badge_form_fields.php",
        "templates/partials/gear_edit_modal.php",
        "templates/partials/gear_form_fields.php",
        "templates/partials/goal_edit_modal.php",
        "templates/partials/goal_form_fields.php",
        "templates/partials/notification_row.php",
        "templates/partials/reminder_form_fields.php",
        "templates/races.php",
        "templates/session_detail.php",
        "templates/settings.php",
        "tests/Domain/BadgeImageTest.php",
        "tests/Domain/BadgeInputTest.php",
        "tests/Domain/CumulativeBadgesTest.php",
        "tests/Domain/DistanceBadgesTest.php",
        "tests/Domain/GearInputTest.php",
        "tests/Domain/GearTagsInputTest.php",
        "tests/Domain/ReminderTest.php",
        "tests/Http/Sessions/SessionPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "CLAUDE.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "References/design/assets/css/components.css",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 2240,
          "deletions": 0,
          "lines_changed": 2240
        },
        {
          "path": "compose.yaml",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "config/routes.php",
          "insertions": 37,
          "deletions": 8,
          "lines_changed": 45
        },
        {
          "path": "config/settings.php",
          "insertions": 12,
          "deletions": 1,
          "lines_changed": 13
        },
        {
          "path": "db/migrations/029_add_reminder_alert_time.sql",
          "insertions": 22,
          "deletions": 0,
          "lines_changed": 22
        },
        {
          "path": "db/migrations/030_add_alerts_archiving.sql",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "db/migrations/031_rename_alerts_to_notifications.sql",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "db/migrations/032_add_badges_table.sql",
          "insertions": 48,
          "deletions": 0,
          "lines_changed": 48
        },
        {
          "path": "db/migrations/033_add_goal_performance_type.sql",
          "insertions": 47,
          "deletions": 0,
          "lines_changed": 47
        },
        {
          "path": "db/migrations/034_add_goal_performance_target_date.sql",
          "insertions": 43,
          "deletions": 0,
          "lines_changed": 43
        },
        {
          "path": "db/migrations/035_add_badge_type.sql",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "db/migrations/036_add_badge_window.sql",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "db/migrations/037_add_badge_ignores.sql",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "db/migrations/038_add_gear_product_fields.sql",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "db/migrations/039_add_gear_tags.sql",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 47,
          "deletions": 2,
          "lines_changed": 49
        },
        {
          "path": "public/assets/img/badges/placeholder.svg",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "public/assets/js/alerts.js",
          "insertions": 0,
          "deletions": 64,
          "lines_changed": 64
        },
        {
          "path": "public/assets/js/notifications.js",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "public/assets/js/viz.js",
          "insertions": 36,
          "deletions": 2,
          "lines_changed": 38
        },
        {
          "path": "src/Domain/Health/Reminder.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "src/Domain/Training/BadgeImage.php",
          "insertions": 79,
          "deletions": 0,
          "lines_changed": 79
        },
        {
          "path": "src/Domain/Training/BadgeInput.php",
          "insertions": 231,
          "deletions": 0,
          "lines_changed": 231
        },
        {
          "path": "src/Domain/Training/CumulativeBadges.php",
          "insertions": 122,
          "deletions": 0,
          "lines_changed": 122
        },
        {
          "path": "src/Domain/Training/DistanceBadges.php",
          "insertions": 0,
          "deletions": 126,
          "lines_changed": 126
        },
        {
          "path": "src/Domain/Training/GearInput.php",
          "insertions": 101,
          "deletions": 49,
          "lines_changed": 150
        },
        {
          "path": "src/Domain/Training/GearTagsInput.php",
          "insertions": 56,
          "deletions": 0,
          "lines_changed": 56
        },
        {
          "path": "src/Http/Admin/AdminBadgesController.php",
          "insertions": 390,
          "deletions": 0,
          "lines_changed": 390
        },
        {
          "path": "src/Http/Admin/InvitationsController.php",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Admin/LogsController.php",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Alerts/AlertGenerator.php",
          "insertions": 0,
          "deletions": 119,
          "lines_changed": 119
        },
        {
          "path": "src/Http/Alerts/AlertsController.php",
          "insertions": 0,
          "deletions": 126,
          "lines_changed": 126
        },
        {
          "path": "src/Http/Alerts/AlertsSidebarCounter.php",
          "insertions": 0,
          "deletions": 30,
          "lines_changed": 30
        },
        {
          "path": "src/Http/Calendar/CalendarController.php",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 396,
          "deletions": 16,
          "lines_changed": 412
        },
        {
          "path": "src/Http/Goals/GoalFormHelpers.php",
          "insertions": 339,
          "deletions": 0,
          "lines_changed": 339
        },
        {
          "path": "src/Http/Goals/GoalsController.php",
          "insertions": 172,
          "deletions": 367,
          "lines_changed": 539
        },
        {
          "path": "src/Http/Goals/GoalsSidebarCounter.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 314,
          "deletions": 0,
          "lines_changed": 314
        },
        {
          "path": "src/Http/Goals/RacesSidebarCounter.php",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 211,
          "deletions": 40,
          "lines_changed": 251
        },
        {
          "path": "src/Http/Notifications/NotificationGenerator.php",
          "insertions": 403,
          "deletions": 0,
          "lines_changed": 403
        },
        {
          "path": "src/Http/Notifications/NotificationsController.php",
          "insertions": 255,
          "deletions": 0,
          "lines_changed": 255
        },
        {
          "path": "src/Http/Notifications/NotificationsSidebarCounter.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "src/Http/Profile/ProfileController.php",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Records/BadgeCatalog.php",
          "insertions": 203,
          "deletions": 0,
          "lines_changed": 203
        },
        {
          "path": "src/Http/Records/BadgesController.php",
          "insertions": 383,
          "deletions": 37,
          "lines_changed": 420
        },
        {
          "path": "src/Http/Records/BadgesSidebarCounter.php",
          "insertions": 26,
          "deletions": 10,
          "lines_changed": 36
        },
        {
          "path": "src/Http/Records/RecordsController.php",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 23,
          "deletions": 3,
          "lines_changed": 26
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 178,
          "deletions": 0,
          "lines_changed": 178
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 32,
          "deletions": 7,
          "lines_changed": 39
        },
        {
          "path": "templates/admin_badges.php",
          "insertions": 163,
          "deletions": 0,
          "lines_changed": 163
        },
        {
          "path": "templates/alerts.php",
          "insertions": 0,
          "deletions": 72,
          "lines_changed": 72
        },
        {
          "path": "templates/badges.php",
          "insertions": 248,
          "deletions": 14,
          "lines_changed": 262
        },
        {
          "path": "templates/gear.php",
          "insertions": 17,
          "deletions": 2,
          "lines_changed": 19
        },
        {
          "path": "templates/gear_detail.php",
          "insertions": 219,
          "deletions": 0,
          "lines_changed": 219
        },
        {
          "path": "templates/goals.php",
          "insertions": 39,
          "deletions": 40,
          "lines_changed": 79
        },
        {
          "path": "templates/health.php",
          "insertions": 67,
          "deletions": 40,
          "lines_changed": 107
        },
        {
          "path": "templates/notifications.php",
          "insertions": 109,
          "deletions": 0,
          "lines_changed": 109
        },
        {
          "path": "templates/partials/admin_tabs.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 41,
          "deletions": 12,
          "lines_changed": 53
        },
        {
          "path": "templates/partials/app_topbar.php",
          "insertions": 11,
          "deletions": 5,
          "lines_changed": 16
        },
        {
          "path": "templates/partials/badge_edit_modal.php",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        },
        {
          "path": "templates/partials/badge_form_fields.php",
          "insertions": 125,
          "deletions": 0,
          "lines_changed": 125
        },
        {
          "path": "templates/partials/gear_edit_modal.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "templates/partials/gear_form_fields.php",
          "insertions": 27,
          "deletions": 1,
          "lines_changed": 28
        },
        {
          "path": "templates/partials/goal_edit_modal.php",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "templates/partials/goal_form_fields.php",
          "insertions": 91,
          "deletions": 45,
          "lines_changed": 136
        },
        {
          "path": "templates/partials/notification_row.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "templates/partials/reminder_form_fields.php",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        },
        {
          "path": "templates/races.php",
          "insertions": 210,
          "deletions": 0,
          "lines_changed": 210
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 57,
          "deletions": 1,
          "lines_changed": 58
        },
        {
          "path": "templates/settings.php",
          "insertions": 133,
          "deletions": 89,
          "lines_changed": 222
        },
        {
          "path": "tests/Domain/BadgeImageTest.php",
          "insertions": 113,
          "deletions": 0,
          "lines_changed": 113
        },
        {
          "path": "tests/Domain/BadgeInputTest.php",
          "insertions": 304,
          "deletions": 0,
          "lines_changed": 304
        },
        {
          "path": "tests/Domain/CumulativeBadgesTest.php",
          "insertions": 301,
          "deletions": 0,
          "lines_changed": 301
        },
        {
          "path": "tests/Domain/DistanceBadgesTest.php",
          "insertions": 0,
          "deletions": 175,
          "lines_changed": 175
        },
        {
          "path": "tests/Domain/GearInputTest.php",
          "insertions": 95,
          "deletions": 0,
          "lines_changed": 95
        },
        {
          "path": "tests/Domain/GearTagsInputTest.php",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        },
        {
          "path": "tests/Domain/ReminderTest.php",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 142,
          "deletions": 0,
          "lines_changed": 142
        }
      ],
      "files_changed": 88,
      "insertions": 9715,
      "deletions": 1520
    },
    {
      "hash": "d2b73b6d48dbe64788b3b3e060ca7d51d773146b",
      "short_hash": "d2b73b6",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1784377804,
      "message": "Add toast notifications for alerts at login and while logged in\n\nSPOR-230 - client-side polling (fetch every 60s) is the only option on\nthis shared-hosting setup, no WebSockets or push possible. Each alert\nis toasted exactly once (toasted_at), queued sequentially since the\nmaquette's .rv-toast component only has one slot.",
      "message_short": "Add toast notifications for alerts at login and while logged in",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "public/assets/js/alerts.js",
        "src/Http/Alerts/AlertsController.php",
        "templates/partials/app_topbar.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 64,
          "deletions": 0,
          "lines_changed": 64
        },
        {
          "path": "config/routes.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "public/assets/js/alerts.js",
          "insertions": 64,
          "deletions": 0,
          "lines_changed": 64
        },
        {
          "path": "src/Http/Alerts/AlertsController.php",
          "insertions": 49,
          "deletions": 0,
          "lines_changed": 49
        },
        {
          "path": "templates/partials/app_topbar.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        }
      ],
      "files_changed": 6,
      "insertions": 185,
      "deletions": 0
    },
    {
      "hash": "bfdc9a6a3baa7883b6c8e630204fa4dfbc44db2e",
      "short_hash": "bfdc9a6",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1784377710,
      "message": "Add Alerts page with unread badge (weight + reminder triggers)\n\nSPOR-229 - first table in the app to persist a user read-state,\ndeliberately breaking from the epic's \"computed at display time, never\nstored\" convention since a read alert must stay read across requests.\nGenerated idempotently (unique key on user+source) on every\nauthenticated page load.",
      "message_short": "Add Alerts page with unread badge (weight + reminder triggers)",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/028_add_alerts.sql",
        "public/assets/css/app.css",
        "src/Domain/Health/Reminder.php",
        "src/Http/Admin/InvitationsController.php",
        "src/Http/Admin/LogsController.php",
        "src/Http/Alerts/AlertGenerator.php",
        "src/Http/Alerts/AlertsController.php",
        "src/Http/Alerts/AlertsSidebarCounter.php",
        "src/Http/Calendar/CalendarController.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Gear/GearController.php",
        "src/Http/Goals/GoalsController.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Profile/ProfileController.php",
        "src/Http/Records/BadgesController.php",
        "src/Http/Records/RecordsController.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Http/Settings/SettingsController.php",
        "templates/alerts.php",
        "templates/partials/app_sidebar.php",
        "tests/Domain/ReminderTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 84,
          "deletions": 0,
          "lines_changed": 84
        },
        {
          "path": "config/routes.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "db/migrations/028_add_alerts.sql",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "src/Domain/Health/Reminder.php",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "src/Http/Admin/InvitationsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Admin/LogsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Alerts/AlertGenerator.php",
          "insertions": 119,
          "deletions": 0,
          "lines_changed": 119
        },
        {
          "path": "src/Http/Alerts/AlertsController.php",
          "insertions": 77,
          "deletions": 0,
          "lines_changed": 77
        },
        {
          "path": "src/Http/Alerts/AlertsSidebarCounter.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "src/Http/Calendar/CalendarController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Goals/GoalsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Profile/ProfileController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Records/BadgesController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Records/RecordsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "templates/alerts.php",
          "insertions": 72,
          "deletions": 0,
          "lines_changed": 72
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "tests/Domain/ReminderTest.php",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        }
      ],
      "files_changed": 25,
      "insertions": 501,
      "deletions": 0
    },
    {
      "hash": "42906fcd2863c85dddd5bfab1ec836d4cb4bfef3",
      "short_hash": "42906fc",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1784373264,
      "message": "Health: capture the time of a reminder, not just the date\n\nSPOR-227 - renames MeasuredAt to DateTimeInput (SPOR-226's date+time\nparser) and reuses it for health_reminders.due_date instead of\nduplicating the same logic. A reminder left without a time shows no\ntime in its hint, rather than a misleading \"00:00\".",
      "message_short": "Health: capture the time of a reminder, not just the date",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/027_add_reminder_time.sql",
        "src/Domain/Health/DateTimeInput.php",
        "src/Domain/Health/MeasuredAt.php",
        "src/Http/Health/HealthController.php",
        "templates/health.php",
        "tests/Domain/DateTimeInputTest.php",
        "tests/Domain/MeasuredAtTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 59,
          "deletions": 0,
          "lines_changed": 59
        },
        {
          "path": "db/migrations/027_add_reminder_time.sql",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "src/Domain/Health/DateTimeInput.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "src/Domain/Health/MeasuredAt.php",
          "insertions": 0,
          "deletions": 28,
          "lines_changed": 28
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 18,
          "deletions": 7,
          "lines_changed": 25
        },
        {
          "path": "templates/health.php",
          "insertions": 18,
          "deletions": 11,
          "lines_changed": 29
        },
        {
          "path": "tests/Domain/DateTimeInputTest.php",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "tests/Domain/MeasuredAtTest.php",
          "insertions": 0,
          "deletions": 37,
          "lines_changed": 37
        }
      ],
      "files_changed": 9,
      "insertions": 171,
      "deletions": 83
    },
    {
      "hash": "77cc6938b72070df6e7027a294c393b9fc28e5c0",
      "short_hash": "77cc693",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1784372677,
      "message": "Health: capture the time of a measure, not just the date\n\nSPOR-226 - measured_at becomes DATETIME; MeasuredAt::parse() combines\nthe new optional Time field with the existing Date field (empty time\ndefaults to midnight, a malformed one is rejected).",
      "message_short": "Health: capture the time of a measure, not just the date",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/026_add_measured_at_time.sql",
        "src/Domain/Health/MeasuredAt.php",
        "src/Http/Health/HealthController.php",
        "templates/health.php",
        "tests/Domain/MeasuredAtTest.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": "db/migrations/026_add_measured_at_time.sql",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "src/Domain/Health/MeasuredAt.php",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 9,
          "deletions": 5,
          "lines_changed": 14
        },
        {
          "path": "templates/health.php",
          "insertions": 15,
          "deletions": 3,
          "lines_changed": 18
        },
        {
          "path": "tests/Domain/MeasuredAtTest.php",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        }
      ],
      "files_changed": 7,
      "insertions": 153,
      "deletions": 8
    },
    {
      "hash": "c900034598f55755ae70da24ee173d9e4195e34f",
      "short_hash": "c900034",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1784371614,
      "message": "Health: fix unreadable select options and pair systolic/diastolic fields\n\nNative <option> popups weren't picking up color-scheme: dark in every\nbrowser (pre-existing bug across all selects, just surfaced here) —\nadded explicit background/color on select.rv-input option. Also moved\nDiastolic next to Systolic instead of stacking it under Date, and swap\nthe value-field icon to droplet for Blood pressure.",
      "message_short": "Health: fix unreadable select options and pair systolic/diastolic fields",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "public/assets/css/components.css",
        "templates/health.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "templates/health.php",
          "insertions": 22,
          "deletions": 14,
          "lines_changed": 36
        }
      ],
      "files_changed": 3,
      "insertions": 55,
      "deletions": 14
    },
    {
      "hash": "83af27501c175458d3070fc37f5ba708922c7f5b",
      "short_hash": "83af275",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1784371044,
      "message": "Health: add blood pressure (systolic/diastolic) measure\n\nSPOR-225 - only measure needing two values per reading, added a\nnullable value2 column rather than a new table to keep the existing\none-type-per-row model.",
      "message_short": "Health: add blood pressure (systolic/diastolic) measure",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/025_add_blood_pressure.sql",
        "src/Domain/Health/HealthMeasure.php",
        "src/Domain/Health/HealthMeasureType.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/HealthMeasureRepository.php",
        "templates/health.php",
        "tests/Domain/HealthMeasureTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 88,
          "deletions": 0,
          "lines_changed": 88
        },
        {
          "path": "db/migrations/025_add_blood_pressure.sql",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "src/Domain/Health/HealthMeasure.php",
          "insertions": 30,
          "deletions": 1,
          "lines_changed": 31
        },
        {
          "path": "src/Domain/Health/HealthMeasureType.php",
          "insertions": 53,
          "deletions": 27,
          "lines_changed": 80
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 34,
          "deletions": 5,
          "lines_changed": 39
        },
        {
          "path": "src/Http/Health/HealthMeasureRepository.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "templates/health.php",
          "insertions": 26,
          "deletions": 1,
          "lines_changed": 27
        },
        {
          "path": "tests/Domain/HealthMeasureTest.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        }
      ],
      "files_changed": 9,
      "insertions": 290,
      "deletions": 36
    },
    {
      "hash": "0a3807dfe8619ba9ead79be8642b94cef268733e",
      "short_hash": "0a3807d",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1784325545,
      "message": "Add Sante (measures/injuries/reminders) and Profil pages\n\nSPOR-219/221-224 (V2.1) - body profile, measures with Chart.js trend\nsparklines, injury log, and schedule-based reminders (no cron in prod).\nSPOR-218/220 (V1.4) - identity, Reve, season stats and Sante/Badges\nteasers, reusing HealthMeasureRepository/MeasureTrend for the teaser\ncards. Replaces the never-detailed SPOR-160 subtask.",
      "message_short": "Add Sante (measures/injuries/reminders) and Profil pages",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/024_add_health.sql",
        "public/assets/js/health-trends.js",
        "src/Domain/Health/BasalMetabolism.php",
        "src/Domain/Health/BiologicalSex.php",
        "src/Domain/Health/HealthMeasure.php",
        "src/Domain/Health/HealthMeasureType.php",
        "src/Domain/Health/Injury.php",
        "src/Domain/Health/MeasureTrend.php",
        "src/Domain/Health/Reminder.php",
        "src/Domain/Health/ReminderBadge.php",
        "src/Domain/Health/ReminderSchedule.php",
        "src/Domain/Health/WeightAlert.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/HealthMeasureRepository.php",
        "src/Http/Profile/ProfileController.php",
        "src/Http/Settings/SettingsController.php",
        "templates/health.php",
        "templates/partials/app_sidebar.php",
        "templates/partials/app_topbar.php",
        "templates/profile.php",
        "templates/settings.php",
        "tests/Domain/BasalMetabolismTest.php",
        "tests/Domain/HealthMeasureTest.php",
        "tests/Domain/InjuryTest.php",
        "tests/Domain/MeasureTrendTest.php",
        "tests/Domain/ReminderBadgeTest.php",
        "tests/Domain/ReminderTest.php",
        "tests/Domain/WeightAlertTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 166,
          "deletions": 0,
          "lines_changed": 166
        },
        {
          "path": "config/routes.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "db/migrations/024_add_health.sql",
          "insertions": 77,
          "deletions": 0,
          "lines_changed": 77
        },
        {
          "path": "public/assets/js/health-trends.js",
          "insertions": 52,
          "deletions": 0,
          "lines_changed": 52
        },
        {
          "path": "src/Domain/Health/BasalMetabolism.php",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "src/Domain/Health/BiologicalSex.php",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "src/Domain/Health/HealthMeasure.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "src/Domain/Health/HealthMeasureType.php",
          "insertions": 75,
          "deletions": 0,
          "lines_changed": 75
        },
        {
          "path": "src/Domain/Health/Injury.php",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "src/Domain/Health/MeasureTrend.php",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "src/Domain/Health/Reminder.php",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "src/Domain/Health/ReminderBadge.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "src/Domain/Health/ReminderSchedule.php",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "src/Domain/Health/WeightAlert.php",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 481,
          "deletions": 0,
          "lines_changed": 481
        },
        {
          "path": "src/Http/Health/HealthMeasureRepository.php",
          "insertions": 43,
          "deletions": 0,
          "lines_changed": 43
        },
        {
          "path": "src/Http/Profile/ProfileController.php",
          "insertions": 99,
          "deletions": 0,
          "lines_changed": 99
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 79,
          "deletions": 5,
          "lines_changed": 84
        },
        {
          "path": "templates/health.php",
          "insertions": 372,
          "deletions": 0,
          "lines_changed": 372
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/partials/app_topbar.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/profile.php",
          "insertions": 161,
          "deletions": 0,
          "lines_changed": 161
        },
        {
          "path": "templates/settings.php",
          "insertions": 45,
          "deletions": 3,
          "lines_changed": 48
        },
        {
          "path": "tests/Domain/BasalMetabolismTest.php",
          "insertions": 52,
          "deletions": 0,
          "lines_changed": 52
        },
        {
          "path": "tests/Domain/HealthMeasureTest.php",
          "insertions": 48,
          "deletions": 0,
          "lines_changed": 48
        },
        {
          "path": "tests/Domain/InjuryTest.php",
          "insertions": 49,
          "deletions": 0,
          "lines_changed": 49
        },
        {
          "path": "tests/Domain/MeasureTrendTest.php",
          "insertions": 76,
          "deletions": 0,
          "lines_changed": 76
        },
        {
          "path": "tests/Domain/ReminderBadgeTest.php",
          "insertions": 52,
          "deletions": 0,
          "lines_changed": 52
        },
        {
          "path": "tests/Domain/ReminderTest.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "tests/Domain/WeightAlertTest.php",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        }
      ],
      "files_changed": 31,
      "insertions": 2265,
      "deletions": 10
    },
    {
      "hash": "1cb8568f35c8b91027d560c2aa8ffc5e6e1b3f11",
      "short_hash": "1cb8568",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1784324805,
      "message": "Records: always show the absolute PR and year PR badge together\n\nSPOR-217 - reverts the SPOR-211/215 dedup: a session holding both its\nsport's absolute record and its own year's record now shows both\nbadges, per explicit user feedback on /sessions/94.",
      "message_short": "Records: always show the absolute PR and year PR badge together",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "src/Http/Records/SessionRecordBadge.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": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "src/Http/Records/SessionRecordBadge.php",
          "insertions": 5,
          "deletions": 9,
          "lines_changed": 14
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 6,
          "deletions": 3,
          "lines_changed": 9
        }
      ],
      "files_changed": 4,
      "insertions": 45,
      "deletions": 12
    }
  ]
}