Skip to content

Corrige les findings M1-M18 de la revue de code du 25/09 - #179

Merged
Oasis35 merged 4 commits into
mainfrom
fix/review-medium-findings
Sep 27, 2026
Merged

Oasis35 merged 4 commits into
mainfrom
fix/review-medium-findings

Conversation

@Oasis35

@Oasis35 Oasis35 commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Résumé

Corrige 9 findings de sévérité "medium" de la revue de code du 2026-09-25 (M1, M2, M3, M4, M6, M11, M12, M13, M14) ainsi que M17 (durcissement CI/CD déploiement) et M18 (Dependabot NuGet mal configuré). Détail complet de chaque correctif dans CLAUDE.md (pièges 35 à 43).

Backend

  • M1 — PATCH /api/sessions/{id}/listening vérifie désormais que le TrackId appartient au défi et refuse de déplacer le verrou anti-triche tant que le morceau courant n'a pas été répondu (contournement possible de la pénalité d'indice / du plancher de durée écoutée).
  • M2 — la requête Deezer tapée par le joueur n'est plus loguée en clair sur échec (seule sa longueur l'est).
  • M3 — CookieAuthService ne catch plus les erreurs DB transitoires comme un cookie invalide (le try/catch ne couvre plus que le déchiffrement).
  • M4 — la complétion de session compare désormais le nombre de réponses au nombre réel de morceaux du défi, plus à la valeur courante (modifiable à chaud) de Settings.TracksPerChallenge.
  • M6 — la déconnexion révoque désormais l'AuthToken côté serveur (rotation) : un cookie intercepté avant logout ne peut plus être rejoué.

Frontend

  • M11/M12 — AudioPlayerService.extend() pendant le chargement n'écourte plus l'écoute sur l'ancienne durée ; nouveau replayCurrent() qui ne remet plus wasExtended/extended à zéro (faussait ExtendedRate côté admin).
  • M13 — apiStatsToday() ne remonte plus en double (interceptor HTTP + GlobalErrorHandler) sur un 5xx transitoire.
  • M14 — la recherche Deezer de la modale d'écoute admin annule désormais explicitement toute requête en vol à la fermeture ou à la réouverture sur un autre morceau (takeUntilDestroyed seul ne suffisait pas, scopé au service et non à la modale).

CI/CD

  • M17 — clé d'hôte SSH épinglable via un secret optionnel (VPS_SSH_HOST_KEY, fallback ssh-keyscan + warning si absent), groupe de concurrency dédié au job deploy (un déploiement en cours n'est plus annulable par un nouveau push sur main), nettoyage des images Docker et vérification santé post-déploiement (API + front) extraits dans deploy/vps/deploy.sh.
  • M18 — directory NuGet de .github/dependabot.yml pointait sur la racine du repo (aucun .csproj dessus) au lieu de src/back : aucune PR de mise à jour NuGet n'était donc jamais générée.

Tests

  • Backend : impossible de compiler/exécuter localement dans cet environnement (pas de SDK .NET ni de Docker disponibles ici) — changements revus manuellement avec soin, tests unitaires/intégration ajoutés pour M3 et M6, laissés à la CI pour validation.
  • Frontend : suite complète (ng test) exécutée localement, 450/450 tests passent, dont les nouveaux tests couvrant M11/M12 (audio-player.service.spec.ts, lecture <audio> réelle) et M14 (admin-pool.service.spec.ts, souscriptions annulées via Subject contrôlé).

Plan de test

  • Suite de tests unitaires front (450/450)
  • CI complète (back, unit-tests, front, unit-tests-front, nginx-headers, integration-tests, e2e)
  • .github/dependabot.yml : vérifier qu'une prochaine exécution Dependabot détecte bien les dépendances NuGet de src/back
  • ⚠️ Action manuelle requise après merge : créer le secret GitHub VPS_SSH_HOST_KEY (ligne ssh-keyscan récupérée une fois sur le VPS puis vérifiée hors bande) pour activer l'épinglage de la clé d'hôte SSH du job deploy — en son absence, le déploiement continue de fonctionner via l'ancien ssh-keyscan (avec un ::warning:: dans les logs CI), aucun risque de casse.

Anti-cheat (M1) : PATCH /sessions/{id}/listening vérifie désormais que le
TrackId appartient au défi et refuse de déplacer le verrou tant que le
morceau courant n'a pas été répondu (contournement possible de la pénalité
d'indice / du plancher de durée écoutée).

Confidentialité (M2) : la requête Deezer tapée par le joueur n'est plus
loguée en clair en cas d'échec (seule sa longueur l'est).

Robustesse (M3) : CookieAuthService ne catch plus les erreurs DB
transitoires comme un cookie invalide (seule l'opération de déchiffrement
reste dans le try/catch).

Complétion de session (M4) : SubmitAnswer compare désormais le nombre de
réponses au nombre réel de morceaux du défi, plus à la valeur courante
(modifiable à chaud) de Settings.TracksPerChallenge.

Sécurité (M6) : la déconnexion révoque désormais l'AuthToken côté serveur
(rotation), un cookie intercepté avant logout ne peut plus être rejoué.

AudioPlayerService (M11/M12) : extend() pendant le chargement n'écourte
plus l'écoute sur l'ancienne durée ; nouveau replayCurrent() qui ne remet
plus wasExtended/extended à zéro (faussait ExtendedRate en admin).

Stats (M13) : apiStatsToday() ne remonte plus en double (interceptor +
GlobalErrorHandler) sur un 5xx transitoire.

Admin pool (M14) : la recherche Deezer de la modale d'écoute annule
désormais explicitement toute requête encore en vol à la fermeture ou à
la réouverture sur un autre morceau (takeUntilDestroyed seul ne suffisait
pas, scopé au service et non à la modale).

CI/CD (M17) : clé d'hôte SSH épinglable via secret (fallback ssh-keyscan
avec warning), concurrency dédiée au job deploy (un déploiement en cours
n'est plus annulable par un nouveau push), nettoyage des images Docker et
vérification santé post-déploiement extraits dans deploy/vps/deploy.sh.

Dependabot (M18) : le directory NuGet pointait sur la racine du repo (pas
de .csproj dessus) au lieu de src/back — aucune PR de mise à jour NuGet
n'était donc jamais générée.

Détail de chaque correctif documenté dans CLAUDE.md (pièges 35-43).
- CookieAuthServiceTests : le cookie de test utilisait un Base64 standard
  (+/=/) alors que Unprotect(string) décode en Base64URL — le FormatException
  levé par le décodage masquait le test avant même d'atteindre le mock, qui
  vérifiait alors le catch existant plutôt que la propagation attendue.

- UpdateListeningHandlerTests : le handler vérifie désormais que le TrackId
  appartient au défi (M1) — SeedAsync crée les DailyChallengeTracks 42/99
  utilisés par les tests. Handle_DifferentTrack_ResetsMin réécrit en
  Handle_DifferentTrack_WhileLockedAndUnanswered_ReturnsConflict : son
  ancienne assertion (déplacer le verrou silencieusement) encodait
  exactement le comportement que M1 corrige. Ajout d'un test 404 sur un
  TrackId inconnu du défi.

- SubmitAnswerHandlerTests : Handle_WhenNotLastAnswer_StatusRemainsActive
  s'appuyait sur l'ancien calcul de complétion (Settings.TracksPerChallenge
  face à un défi à 1 seul morceau) — désormais basé sur le nombre réel de
  DailyChallengeTracks (M4), le défi de test porte deux morceaux.
…on en CryptographicException

L'extension ASP.NET Core IDataProtector.Unprotect(string) enveloppe toute
exception non-CryptographicException levée par Unprotect(byte[]) dans...
une CryptographicException. Un mock qui throw à cet endroit atterrissait
donc toujours dans notre propre catch restreint, jamais en propagation —
le test passait localement par erreur de raisonnement, jamais vérifié en
CI avant ce point (pas de SDK .NET disponible en sandbox).

Remplacé par un scénario qui simule réellement une panne DB (ObjectDisposedException
sur un DbContext disposé) après un déchiffrement réussi — le seul endroit
qui peut légitimement laisser fuiter un type d'exception arbitraire hors
du try/catch restreint à Unprotect/Guid.TryParse.
Utilise [[ ]] plutôt que [ ] pour le test conditionnel — plus sûr et
plus riche en bash (pas de risque de word-splitting sur les opérateurs).
@sonarqubecloud

Copy link
Copy Markdown

@Oasis35
Oasis35 merged commit 95743ad into main Sep 27, 2026
17 checks passed
@Oasis35
Oasis35 deleted the fix/review-medium-findings branch September 27, 2026 14:56
Oasis35 added a commit that referenced this pull request Sep 30, 2026
…ls cassé) (#239)

Tous les appareils d'un compte v1 partagent le même AuthToken : le faire
tourner à la déconnexion (M6, #179) déconnectait tous les autres appareils.
La déconnexion n'efface plus que le cookie de l'appareil courant.
Oasis35 added a commit that referenced this pull request Oct 1, 2026
* Envoi des emails via Brevo au lieu de Resend (#197)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Staging → main : compteur de gels corrigé (#199)

* Envoi des emails via Brevo au lieu de Resend (#196)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Gel de série : retirer du compteur les gels déjà engagés sur une série protégée (#198)

La gélule du header affichait encore le stock en base (ex. 2) alors que l'accueil
annonçait qu'un gel avait sauvé la série : le stock n'est décrémenté qu'à la
complétion du défi. StreakView.Freezes déduit désormais les jours manqués en
statut protected, ce qui correspond au stock qui restera après la partie.

* Déconnexion : ne plus faire tourner le jeton du compte (multi-appareils cassé) (#239)

Tous les appareils d'un compte v1 partagent le même AuthToken : le faire
tourner à la déconnexion (M6, #179) déconnectait tous les autres appareils.
La déconnexion n'efface plus que le cookie de l'appareil courant.

* fix(partage): repli execCommand quand clipboard.writeText échoue (#200) (#241)

Le bouton « Partager mon score » ne copiait parfois rien au premier clic :
navigator.clipboard.writeText rejette de façon intermittente (NotAllowedError,
« Document is not focused »). Sur rejet, ClipboardService retombe sur une copie
textarea + execCommand('copy') et remonte l'échec (sans le texte) à l'API.

* Admin : renommer un morceau du défi du jour depuis le pool (#246)

* feat(admin): renommer un morceau du défi du jour depuis le pool

Retire le verrou RenameLock (409 track_locked) : le bouton ✎ est actif
sur toutes les lignes du pool, défi du jour compris. Les réponses déjà
enregistrées gardent leur verdict, les suivantes sont corrigées avec le
nouveau nom.

* feat(admin): avertissement dans la modale de renommage pour un morceau du défi du jour
Oasis35 added a commit that referenced this pull request Oct 1, 2026
…) (#250)

* Envoi des emails via Brevo au lieu de Resend (#197)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Staging → main : compteur de gels corrigé (#199)

* Envoi des emails via Brevo au lieu de Resend (#196)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Gel de série : retirer du compteur les gels déjà engagés sur une série protégée (#198)

La gélule du header affichait encore le stock en base (ex. 2) alors que l'accueil
annonçait qu'un gel avait sauvé la série : le stock n'est décrémenté qu'à la
complétion du défi. StreakView.Freezes déduit désormais les jours manqués en
statut protected, ce qui correspond au stock qui restera après la partie.

* Déconnexion : ne plus faire tourner le jeton du compte (multi-appareils cassé) (#239)

Tous les appareils d'un compte v1 partagent le même AuthToken : le faire
tourner à la déconnexion (M6, #179) déconnectait tous les autres appareils.
La déconnexion n'efface plus que le cookie de l'appareil courant.

* fix(partage): repli execCommand quand clipboard.writeText échoue (#200) (#241)

Le bouton « Partager mon score » ne copiait parfois rien au premier clic :
navigator.clipboard.writeText rejette de façon intermittente (NotAllowedError,
« Document is not focused »). Sur rejet, ClipboardService retombe sur une copie
textarea + execCommand('copy') et remonte l'échec (sans le texte) à l'API.

* Admin : renommer un morceau du défi du jour depuis le pool (#246)

* feat(admin): renommer un morceau du défi du jour depuis le pool

Retire le verrou RenameLock (409 track_locked) : le bouton ✎ est actif
sur toutes les lignes du pool, défi du jour compris. Les réponses déjà
enregistrées gardent leur verdict, les suivantes sont corrigées avec le
nouveau nom.

* feat(admin): avertissement dans la modale de renommage pour un morceau du défi du jour

* fix(audio): palier coupé avant le son sur iPhone (relecture) (#249)

Safari iOS émet `playing` dès l'appel à play(), avant que le son reparte,
sans second `playing` ensuite : le chrono d'arrêt coupait le palier à
l'aveugle (0,5s → 0,1s de son, voire aucun). Le chrono relit désormais
la position de lecture à son échéance et se reprogramme tant que le
palier n'est pas atteint ; rien ne coupe pendant un seek.

* Bump the nuget-minor-patch group with 5 updates (#245)

Bumps coverlet.collector from 10.0.1 to 10.1.0
Bumps WolverineFx from 6.40.0 to 6.43.0
Bumps WolverineFx.EntityFrameworkCore from 6.40.0 to 6.43.0
Bumps WolverineFx.FluentValidation from 6.40.0 to 6.43.0
Bumps WolverineFx.RuntimeCompilation from 6.40.0 to 6.43.0

---
updated-dependencies:
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.EntityFrameworkCore
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.FluentValidation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.RuntimeCompilation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
...
Oasis35 added a commit that referenced this pull request Oct 2, 2026
* Envoi des emails via Brevo au lieu de Resend (#197)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Staging → main : compteur de gels corrigé (#199)

* Envoi des emails via Brevo au lieu de Resend (#196)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Gel de série : retirer du compteur les gels déjà engagés sur une série protégée (#198)

La gélule du header affichait encore le stock en base (ex. 2) alors que l'accueil
annonçait qu'un gel avait sauvé la série : le stock n'est décrémenté qu'à la
complétion du défi. StreakView.Freezes déduit désormais les jours manqués en
statut protected, ce qui correspond au stock qui restera après la partie.

* Déconnexion : ne plus faire tourner le jeton du compte (multi-appareils cassé) (#239)

Tous les appareils d'un compte v1 partagent le même AuthToken : le faire
tourner à la déconnexion (M6, #179) déconnectait tous les autres appareils.
La déconnexion n'efface plus que le cookie de l'appareil courant.

* fix(partage): repli execCommand quand clipboard.writeText échoue (#200) (#241)

Le bouton « Partager mon score » ne copiait parfois rien au premier clic :
navigator.clipboard.writeText rejette de façon intermittente (NotAllowedError,
« Document is not focused »). Sur rejet, ClipboardService retombe sur une copie
textarea + execCommand('copy') et remonte l'échec (sans le texte) à l'API.

* Admin : renommer un morceau du défi du jour depuis le pool (#246)

* feat(admin): renommer un morceau du défi du jour depuis le pool

Retire le verrou RenameLock (409 track_locked) : le bouton ✎ est actif
sur toutes les lignes du pool, défi du jour compris. Les réponses déjà
enregistrées gardent leur verdict, les suivantes sont corrigées avec le
nouveau nom.

* feat(admin): avertissement dans la modale de renommage pour un morceau du défi du jour

* fix(audio): palier coupé avant le son sur iPhone (relecture) (#249)

Safari iOS émet `playing` dès l'appel à play(), avant que le son reparte,
sans second `playing` ensuite : le chrono d'arrêt coupait le palier à
l'aveugle (0,5s → 0,1s de son, voire aucun). Le chrono relit désormais
la position de lecture à son échéance et se reprogramme tant que le
palier n'est pas atteint ; rien ne coupe pendant un seek.

* Bump the nuget-minor-patch group with 5 updates (#245)

Bumps coverlet.collector from 10.0.1 to 10.1.0
Bumps WolverineFx from 6.40.0 to 6.43.0
Bumps WolverineFx.EntityFrameworkCore from 6.40.0 to 6.43.0
Bumps WolverineFx.FluentValidation from 6.40.0 to 6.43.0
Bumps WolverineFx.RuntimeCompilation from 6.40.0 to 6.43.0

---
updated-dependencies:
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.EntityFrameworkCore
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.FluentValidation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.RuntimeCompilation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Conformité aux conditions de l'API Deezer (#251)

* Conformité aux conditions de l'API Deezer

- Confidentialité : ajoute la mention obligatoire « usage strictement privé,
  dans le cadre familial » (art. IV des conditions Deezer) et corrige le
  passage qui affirmait que Deezer ne reçoit rien : le navigateur charge
  extraits et pochettes depuis les CDN Deezer, qui voient l'adresse IP.
- Stories Instagram : plus de pochette Deezer dans les images (Deezer
  interdit de stocker ses images). L'API du récap hebdo ne renvoie plus de
  coverUrl, une vignette note de musique la remplace.
- Doc : piège 47 (CLAUDE.md) et docs des stories.

Claude-Session: https://claude.ai/code/session_01B3wbknbbcPyZg2tJQRtvNX

* Spec stories : toHaveSize plutôt que length (Sonar S5906)

Claude-Session: https://claude.ai/code/session_01B3wbknbbcPyZg2tJQRtvNX

* docs(v2): lecteur audio Howler.js dans le plan et le plan de développement

AudioPort de la v2 sur Howler.js en Web Audio (décision du 01/10/2026),
CORS du CDN des extraits Deezer vérifié le 02/10/2026, CSP connect-src.
Fichiers repris de env/staging avec ces modifications.

* docs(v2): retire le plan v2 de cette PR (déplacé dans une PR vers env/staging)
Oasis35 added a commit that referenced this pull request Oct 7, 2026
* Envoi des emails via Brevo au lieu de Resend (#197)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Staging → main : compteur de gels corrigé (#199)

* Envoi des emails via Brevo au lieu de Resend (#196)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Gel de série : retirer du compteur les gels déjà engagés sur une série protégée (#198)

La gélule du header affichait encore le stock en base (ex. 2) alors que l'accueil
annonçait qu'un gel avait sauvé la série : le stock n'est décrémenté qu'à la
complétion du défi. StreakView.Freezes déduit désormais les jours manqués en
statut protected, ce qui correspond au stock qui restera après la partie.

* Déconnexion : ne plus faire tourner le jeton du compte (multi-appareils cassé) (#239)

Tous les appareils d'un compte v1 partagent le même AuthToken : le faire
tourner à la déconnexion (M6, #179) déconnectait tous les autres appareils.
La déconnexion n'efface plus que le cookie de l'appareil courant.

* fix(partage): repli execCommand quand clipboard.writeText échoue (#200) (#241)

Le bouton « Partager mon score » ne copiait parfois rien au premier clic :
navigator.clipboard.writeText rejette de façon intermittente (NotAllowedError,
« Document is not focused »). Sur rejet, ClipboardService retombe sur une copie
textarea + execCommand('copy') et remonte l'échec (sans le texte) à l'API.

* Admin : renommer un morceau du défi du jour depuis le pool (#246)

* feat(admin): renommer un morceau du défi du jour depuis le pool

Retire le verrou RenameLock (409 track_locked) : le bouton ✎ est actif
sur toutes les lignes du pool, défi du jour compris. Les réponses déjà
enregistrées gardent leur verdict, les suivantes sont corrigées avec le
nouveau nom.

* feat(admin): avertissement dans la modale de renommage pour un morceau du défi du jour

* fix(audio): palier coupé avant le son sur iPhone (relecture) (#249)

Safari iOS émet `playing` dès l'appel à play(), avant que le son reparte,
sans second `playing` ensuite : le chrono d'arrêt coupait le palier à
l'aveugle (0,5s → 0,1s de son, voire aucun). Le chrono relit désormais
la position de lecture à son échéance et se reprogramme tant que le
palier n'est pas atteint ; rien ne coupe pendant un seek.

* Bump the nuget-minor-patch group with 5 updates (#245)

Bumps coverlet.collector from 10.0.1 to 10.1.0
Bumps WolverineFx from 6.40.0 to 6.43.0
Bumps WolverineFx.EntityFrameworkCore from 6.40.0 to 6.43.0
Bumps WolverineFx.FluentValidation from 6.40.0 to 6.43.0
Bumps WolverineFx.RuntimeCompilation from 6.40.0 to 6.43.0

---
updated-dependencies:
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.EntityFrameworkCore
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.FluentValidation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.RuntimeCompilation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
...
* Conformité aux conditions de l'API Deezer (#251)

* Conformité aux conditions de l'API Deezer

- Confidentialité : ajoute la mention obligatoire « usage strictement privé,
  dans le cadre familial » (art. IV des conditions Deezer) et corrige le
  passage qui affirmait que Deezer ne reçoit rien : le navigateur charge
  extraits et pochettes depuis les CDN Deezer, qui voient l'adresse IP.
- Stories Instagram : plus de pochette Deezer dans les images (Deezer
  interdit de stocker ses images). L'API du récap hebdo ne renvoie plus de
  coverUrl, une vignette note de musique la remplace.
- Doc : piège 47 (CLAUDE.md) et docs des stories.

* Spec stories : toHaveSize plutôt que length (Sonar S5906)

* docs(v2): lecteur audio Howler.js dans le plan et le plan de développement

AudioPort de la v2 sur Howler.js en Web Audio (décision du 01/10/2026),
CORS du CDN des extraits Deezer vérifié le 02/10/2026, CSP connect-src.
Fichiers repris de env/staging avec ces modifications.

* docs(v2): retire le plan v2 de cette PR (déplacé dans une PR vers env/staging)

* Bump brace-expansion in /src/front/InSeconds.Client (#267)

Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.18 to 1.1.21.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](juliangruber/brace-expansion@v1.1.18...v1.1.21)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.21
  dependency-type: indirect
...

* v2 admin : en-tête recopié de la v1 (titre centré, 5 onglets, déconnexion en bas)

- titre et identifiant navigateur centrés, haut de page à 32 px comme en v1
- barre des 5 onglets de la v1 ; ceux de F2 ouvrent une page d'attente sous la barre
- déconnexion sous le contenu de l'onglet
- plan et suivi de la refonte : mise en page, accès à l'admin du staging, usage à 0 jusqu'en E
Oasis35 added a commit that referenced this pull request Oct 11, 2026
* Envoi des emails via Brevo au lieu de Resend (#197)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Staging → main : compteur de gels corrigé (#199)

* Envoi des emails via Brevo au lieu de Resend (#196)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Gel de série : retirer du compteur les gels déjà engagés sur une série protégée (#198)

La gélule du header affichait encore le stock en base (ex. 2) alors que l'accueil
annonçait qu'un gel avait sauvé la série : le stock n'est décrémenté qu'à la
complétion du défi. StreakView.Freezes déduit désormais les jours manqués en
statut protected, ce qui correspond au stock qui restera après la partie.

* Déconnexion : ne plus faire tourner le jeton du compte (multi-appareils cassé) (#239)

Tous les appareils d'un compte v1 partagent le même AuthToken : le faire
tourner à la déconnexion (M6, #179) déconnectait tous les autres appareils.
La déconnexion n'efface plus que le cookie de l'appareil courant.

* fix(partage): repli execCommand quand clipboard.writeText échoue (#200) (#241)

Le bouton « Partager mon score » ne copiait parfois rien au premier clic :
navigator.clipboard.writeText rejette de façon intermittente (NotAllowedError,
« Document is not focused »). Sur rejet, ClipboardService retombe sur une copie
textarea + execCommand('copy') et remonte l'échec (sans le texte) à l'API.

* Admin : renommer un morceau du défi du jour depuis le pool (#246)

* feat(admin): renommer un morceau du défi du jour depuis le pool

Retire le verrou RenameLock (409 track_locked) : le bouton ✎ est actif
sur toutes les lignes du pool, défi du jour compris. Les réponses déjà
enregistrées gardent leur verdict, les suivantes sont corrigées avec le
nouveau nom.

* feat(admin): avertissement dans la modale de renommage pour un morceau du défi du jour

* fix(audio): palier coupé avant le son sur iPhone (relecture) (#249)

Safari iOS émet `playing` dès l'appel à play(), avant que le son reparte,
sans second `playing` ensuite : le chrono d'arrêt coupait le palier à
l'aveugle (0,5s → 0,1s de son, voire aucun). Le chrono relit désormais
la position de lecture à son échéance et se reprogramme tant que le
palier n'est pas atteint ; rien ne coupe pendant un seek.

* Bump the nuget-minor-patch group with 5 updates (#245)

Bumps coverlet.collector from 10.0.1 to 10.1.0
Bumps WolverineFx from 6.40.0 to 6.43.0
Bumps WolverineFx.EntityFrameworkCore from 6.40.0 to 6.43.0
Bumps WolverineFx.FluentValidation from 6.40.0 to 6.43.0
Bumps WolverineFx.RuntimeCompilation from 6.40.0 to 6.43.0

---
updated-dependencies:
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.EntityFrameworkCore
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.FluentValidation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.RuntimeCompilation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
...

* Conformité aux conditions de l'API Deezer (#251)

* Conformité aux conditions de l'API Deezer

- Confidentialité : ajoute la mention obligatoire « usage strictement privé,
  dans le cadre familial » (art. IV des conditions Deezer) et corrige le
  passage qui affirmait que Deezer ne reçoit rien : le navigateur charge
  extraits et pochettes depuis les CDN Deezer, qui voient l'adresse IP.
- Stories Instagram : plus de pochette Deezer dans les images (Deezer
  interdit de stocker ses images). L'API du récap hebdo ne renvoie plus de
  coverUrl, une vignette note de musique la remplace.
- Doc : piège 47 (CLAUDE.md) et docs des stories.

* Spec stories : toHaveSize plutôt que length (Sonar S5906)

* docs(v2): lecteur audio Howler.js dans le plan et le plan de développement

AudioPort de la v2 sur Howler.js en Web Audio (décision du 01/10/2026),
CORS du CDN des extraits Deezer vérifié le 02/10/2026, CSP connect-src.
Fichiers repris de env/staging avec ces modifications.

* docs(v2): retire le plan v2 de cette PR (déplacé dans une PR vers env/staging)

* Bump brace-expansion in /src/front/InSeconds.Client (#267)

Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.18 to 1.1.21.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](juliangruber/brace-expansion@v1.1.18...v1.1.21)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.21
  dependency-type: indirect

* Bump source-map-js from 1.2.1 to 1.2.2 in /src/front/InSeconds.Client (#269)

Bumps [source-map-js](https://github.com/7rulnik/source-map-js) from 1.2.1 to 1.2.2.
- [Release notes](https://github.com/7rulnik/source-map-js/releases)
- [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md)
- [Commits](7rulnik/source-map-js@v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: source-map-js
  dependency-version: 1.2.2
  dependency-type: indirect
..

* Sauvegardes automatiques de la base de prod (timer VPS + R2 + alerte Grafana) (#293)

* Sauvegardes de la base de prod : backup.sh + workflow nocturne

- deploy/infra/backup.sh : pg_dump de la base inseconds (tous schémas), vérification
  (pg_restore --list), 7 jours gardés sur le VPS, envoi chiffré (rclone crypt) vers
  Cloudflare R2 (daily/ 30 j, monthly/ 365 j) et ping healthchecks.io si configurés ;
  sous-commandes list-remote, fetch et restore-test (base jetable).
- .github/workflows/db-backup.yml : lance le script chaque nuit à 3 h UTC et à la
  demande, par SSH (le dump ne passe jamais par GitHub).
- Doc : deploy/infra/README.md § Sauvegardes, .env.example, CLAUDE.md, README FR/EN,
  docs/TACHES.md.

* Sauvegardes : timer systemd du VPS et alerte Grafana au lieu du workflow

Le workflow GitHub Actions est remplacé par un timer systemd (3 h UTC,
Persistent=true) qui lance backup.sh depuis le checkout de prod. Le ping
healthchecks.io est remplacé par un log OTLP envoyé à Grafana Cloud avec la
configuration de l'API ; une alerte Grafana prévient si aucune sauvegarde
réussie n'arrive en 26 h.

* backup.sh : paramètres positionnels nommés (Sonar)

* backup.sh : envoi vers R2 par l'entrée standard (rcat) au lieu d'un dossier monté (#294)

Sur le VPS, le conteneur rclone ne voyait pas le dump dans le dossier monté
(« Source doesn't exist or is a directory »). Le dump passe désormais par
l'entrée standard (rcat) à l'envoi et par la sortie standard (cat) au
téléchargement : plus de montage, plus de dépendance aux droits Docker.

* docs: SSH par clé et par login/mot de passe, tous deux actifs sur le VPS (#296)

* docs: SSH par mot de passe réactivé sur le VPS (accès de secours)

* docs: préciser que clé SSH et login/mot de passe restent tous deux actifs
Oasis35 added a commit that referenced this pull request Oct 12, 2026
* Envoi des emails via Brevo au lieu de Resend (#197)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Staging → main : compteur de gels corrigé (#199)

* Envoi des emails via Brevo au lieu de Resend (#196)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Gel de série : retirer du compteur les gels déjà engagés sur une série protégée (#198)

La gélule du header affichait encore le stock en base (ex. 2) alors que l'accueil
annonçait qu'un gel avait sauvé la série : le stock n'est décrémenté qu'à la
complétion du défi. StreakView.Freezes déduit désormais les jours manqués en
statut protected, ce qui correspond au stock qui restera après la partie.

* Déconnexion : ne plus faire tourner le jeton du compte (multi-appareils cassé) (#239)

Tous les appareils d'un compte v1 partagent le même AuthToken : le faire
tourner à la déconnexion (M6, #179) déconnectait tous les autres appareils.
La déconnexion n'efface plus que le cookie de l'appareil courant.

* fix(partage): repli execCommand quand clipboard.writeText échoue (#200) (#241)

Le bouton « Partager mon score » ne copiait parfois rien au premier clic :
navigator.clipboard.writeText rejette de façon intermittente (NotAllowedError,
« Document is not focused »). Sur rejet, ClipboardService retombe sur une copie
textarea + execCommand('copy') et remonte l'échec (sans le texte) à l'API.

* Admin : renommer un morceau du défi du jour depuis le pool (#246)

* feat(admin): renommer un morceau du défi du jour depuis le pool

Retire le verrou RenameLock (409 track_locked) : le bouton ✎ est actif
sur toutes les lignes du pool, défi du jour compris. Les réponses déjà
enregistrées gardent leur verdict, les suivantes sont corrigées avec le
nouveau nom.

* feat(admin): avertissement dans la modale de renommage pour un morceau du défi du jour

* fix(audio): palier coupé avant le son sur iPhone (relecture) (#249)

Safari iOS émet `playing` dès l'appel à play(), avant que le son reparte,
sans second `playing` ensuite : le chrono d'arrêt coupait le palier à
l'aveugle (0,5s → 0,1s de son, voire aucun). Le chrono relit désormais
la position de lecture à son échéance et se reprogramme tant que le
palier n'est pas atteint ; rien ne coupe pendant un seek.

* Bump the nuget-minor-patch group with 5 updates (#245)

Bumps coverlet.collector from 10.0.1 to 10.1.0
Bumps WolverineFx from 6.40.0 to 6.43.0
Bumps WolverineFx.EntityFrameworkCore from 6.40.0 to 6.43.0
Bumps WolverineFx.FluentValidation from 6.40.0 to 6.43.0
Bumps WolverineFx.RuntimeCompilation from 6.40.0 to 6.43.0

---
updated-dependencies:
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.EntityFrameworkCore
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.FluentValidation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.RuntimeCompilation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
...

* Conformité aux conditions de l'API Deezer (#251)

* Conformité aux conditions de l'API Deezer

- Confidentialité : ajoute la mention obligatoire « usage strictement privé,
  dans le cadre familial » (art. IV des conditions Deezer) et corrige le
  passage qui affirmait que Deezer ne reçoit rien : le navigateur charge
  extraits et pochettes depuis les CDN Deezer, qui voient l'adresse IP.
- Stories Instagram : plus de pochette Deezer dans les images (Deezer
  interdit de stocker ses images). L'API du récap hebdo ne renvoie plus de
  coverUrl, une vignette note de musique la remplace.
- Doc : piège 47 (CLAUDE.md) et docs des stories.

* Spec stories : toHaveSize plutôt que length (Sonar S5906)

* docs(v2): lecteur audio Howler.js dans le plan et le plan de développement

AudioPort de la v2 sur Howler.js en Web Audio (décision du 01/10/2026),
CORS du CDN des extraits Deezer vérifié le 02/10/2026, CSP connect-src.
Fichiers repris de env/staging avec ces modifications.

* docs(v2): retire le plan v2 de cette PR (déplacé dans une PR vers env/staging)

* Bump brace-expansion in /src/front/InSeconds.Client (#267)

Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.18 to 1.1.21.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](juliangruber/brace-expansion@v1.1.18...v1.1.21)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.21
  dependency-type: indirect
...

* Bump source-map-js from 1.2.1 to 1.2.2 in /src/front/InSeconds.Client (#269)

Bumps [source-map-js](https://github.com/7rulnik/source-map-js) from 1.2.1 to 1.2.2.
- [Release notes](https://github.com/7rulnik/source-map-js/releases)
- [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md)
- [Commits](7rulnik/source-map-js@v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: source-map-js
  dependency-version: 1.2.2
  dependency-type: indirect

* Sauvegardes automatiques de la base de prod (timer VPS + R2 + alerte Grafana) (#293)

* Sauvegardes de la base de prod : backup.sh + workflow nocturne

- deploy/infra/backup.sh : pg_dump de la base inseconds (tous schémas), vérification
  (pg_restore --list), 7 jours gardés sur le VPS, envoi chiffré (rclone crypt) vers
  Cloudflare R2 (daily/ 30 j, monthly/ 365 j) et ping healthchecks.io si configurés ;
  sous-commandes list-remote, fetch et restore-test (base jetable).
- .github/workflows/db-backup.yml : lance le script chaque nuit à 3 h UTC et à la
  demande, par SSH (le dump ne passe jamais par GitHub).
- Doc : deploy/infra/README.md § Sauvegardes, .env.example, CLAUDE.md, README FR/EN,
  docs/TACHES.md.

* Sauvegardes : timer systemd du VPS et alerte Grafana au lieu du workflow

Le workflow GitHub Actions est remplacé par un timer systemd (3 h UTC,
Persistent=true) qui lance backup.sh depuis le checkout de prod. Le ping
healthchecks.io est remplacé par un log OTLP envoyé à Grafana Cloud avec la
configuration de l'API ; une alerte Grafana prévient si aucune sauvegarde
réussie n'arrive en 26 h.

* backup.sh : paramètres positionnels nommés (Sonar)

* backup.sh : envoi vers R2 par l'entrée standard (rcat) au lieu d'un dossier monté (#294)

Sur le VPS, le conteneur rclone ne voyait pas le dump dans le dossier monté
(« Source doesn't exist or is a directory »). Le dump passe désormais par
l'entrée standard (rcat) à l'envoi et par la sortie standard (cat) au
téléchargement : plus de montage, plus de dépendance aux droits Docker.

* docs: SSH par clé et par login/mot de passe, tous deux actifs sur le VPS (#296)

* docs: SSH par mot de passe réactivé sur le VPS (accès de secours)

* docs: préciser que clé SSH et login/mot de passe restent tous deux actifs

* Bascule v2 : workflow remplaçant minimal pour le rendre lançable depuis env/staging (#302)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant