Système de notifications in-app (cloche). Schéma de la table notifications : voir entities/communication.md.

Ce qui déclenche réellement une notification

Type (NotificationType) Déclenché par
new_comment comments/services/comment.service.ts (createCommentForMedia)
new_chat_message chats/services/chat-message.service.ts (saveMessage)
media_added, piste_update, new_comment_reply, new_chat_message_reply jamais émis — définis dans l'enum/l'entité mais aucun code ne les déclenche

Les réponses (à un commentaire ou un message) réutilisent le type parent (new_comment/new_chat_message) : le front distingue une réponse via comment.originalCommentId, pas via notification.type.

Groupage (groupCount)

  • Clé de regroupement : chatChannelId > mediaId > pisteId (le premier non-null l'emporte).
  • À la création, si une notification non lue existe déjà pour le même type + user + clé, on incrémente son groupCount et on rafraîchit son createdAt (elle remonte en haut de liste) au lieu de créer une nouvelle ligne.
  • Le groupage ne vaut que tant que la notification reste non lue — une fois lue, l'événement suivant repart sur une nouvelle ligne à groupCount = 1.
  • Pas de transaction ni de verrou : c'est un read-modify-write simple, appelé en fire-and-forget par les deux déclencheurs ci-dessus (non attendu par la requête HTTP). Deux événements quasi simultanés pour la même cible peuvent donc créer deux lignes au lieu d'incrémenter une seule.

Temps réel (SSE)

  • GET /notifications/stream (Server-Sent Events). Le token JWT est passé en query param (?token=) car EventSource ne permet pas d'envoyer des headers custom.
  • Le bus est un Subject RxJS en mémoire, local à l'instance de l'API — si l'API tourne sur plusieurs instances/pods, un client connecté sur l'instance A ne reçoit rien des événements traités par l'instance B.
  • Reconnexion front : backoff exponentiel, abandon silencieux après 5 tentatives (console.error seulement, rien remonté à l'UI).

Lu / non lu

Endpoints (voir bruno/Notifications/) : GET /notifications, GET /notifications/unread, GET /notifications/unread/count, POST /notifications/:id/read, POST /notifications/read-all, POST /notifications/chat-channel/:channelId/read.

  • POST /notifications/:id/read ne vérifie pas que la notification appartient à l'utilisateur courant — seul JwtAuthGuard protège la route, sans filtre WHERE userId = .... Un utilisateur authentifié qui devine un id peut donc marquer comme lue la notification d'un autre.

Piège : deux compteurs "non lu" indépendants pour le chat

Le badge de discussion (pastille sur l’onglet Discussion) doit utiliser unreadChatMessages (calculé via chat_channels_users.lastMessageReadIndex vs chat_channels.lastMessageIndex, exposé par GET /user-notifications). Le badge de la cloche utilise unreadMessages (notifs commentaires non lues, hors types chat).

Ne pas brancher la pastille Discussion sur unreadMessages : sinon elle s’allume dès qu’il y a des réponses à des commentaires, même sans aucune conversation (bug corrigé INSP-480).

Marquer une notification cloche comme lue ne touche pas le compteur chat, et inversement. Les notifications de type chat sont filtrées de la liste affichée dans la cloche — elles n’existent en pratique que pour nourrir le badge Discussion.

Ce n'est pas de l'email ni du push

Purement in-app — aucun email ou push n'est envoyé quand une ligne notifications est créée.

Un mécanisme totalement séparé et indépendant, chats/services/chat-notification.service.ts (crons quotidiens 19h/20h, voir cron-tasks.md), envoie des emails de relance ("vous avez un message non lu depuis N jours") — il ne lit ni n'écrit la table notifications, ne pas confondre les deux systèmes.

INSP-480 — Réponses aux commentaires (comportement actuel)

Quand quelqu'un répond à un commentaire vidéo (originalCommentId renseigné), CommentService.createCommentForMedia crée immédiatement des notifications new_comment (pas new_comment_reply) pour :

  1. le créateur du média ;
  2. les EE dont la piste est liée au média (parcours-eclaireurspistesrelated-medias) ;
  3. l'auteur du commentaire original (réponse) ;

puis exclut l'auteur de la nouvelle réponse. Livraison : SSE à la création + backfill syncCommentNotifications à la connexion SSE. Pas de cron horaire.

UI cloche (NotificationItem.vue) : avatar, prénom gras + « a répondu à ton commentaire » si comment.originalCommentId, durée relative (formatDateDifference), extrait du texte, clic → /inspiration?mediaId=&openComments=true&commentId=.

Écarts vs critères d'acceptation INSP-480

Attendu CA État actuel
1 notification par réponse Groupage : 1 ligne / mediaId non lu (groupCount)
Type dédié / wording via type Toujours new_comment ; wording déduit de originalCommentId
Réponse complète Texte tronqué à 100 caractères dans la cloche
Tag piste / article Chip si piste?.name ou media?.piste?.name ; à la création pas de pisteId ; l'API joint media.relatedMedias.piste (pas media.piste) — chip souvent absent ; pas de fallback article
Fil de discussion en haut de la liste Scroll + highlight 2s seulement ; ordre chronologique inchangé
Batch toutes les heures (option CA) Non implémenté (immédiat uniquement)

Jeu de données local (demo)

cd api && yarn console seed-comment-notifications-demo
# ou ciblé :
yarn console seed-comment-notifications-demo vlecsei@gmail.com

# Discussions (chat) — pastille Discussion indépendante de la cloche :
yarn console seed-chat-discussions-demo vlecsei@gmail.com

Crée des comptes demo-notif-*@inspire.local (mdp DemoNotif480!), un média lié à une piste, commentaires / réponses et notifs via CommentService. Affiche les URLs deep link en fin de run. Voir le script api/src/core/scripts/seed-comment-notifications-demo.ts.

Checklist manuelle : login LY → cloche (wording réponse, avatar, durée) → clic deep link → modale + highlight ; login EE → parcours inverse ; 2e réponse non lue → groupCount ; après lecture + nouvelle réponse → nouvelle ligne.

Historique

La migration fix-notifications-media-fk a corrigé une FK qui pointait par erreur vers related-medias au lieu de medias, ce qui empêchait la création d'une notification de commentaire dès que le média commenté n'était pas coïncidentellement aussi un id valide dans related-medias.

core/users/services/notification.service.ts existe encore dans le code mais n'est appelé nulle part (module mort, superseded par core/notifications) — ne pas s'y fier si on cherche où les notifications sont réellement créées.