Files
oxspeak_server/.junie/plans/fix-new-member-history.md
T
2026-10-18 18:46:52 +02:00

11 KiB

sessionId
sessionId
session-261018-151548-e6aw

Requirements

Overview & Goals

Quand un nouvel utilisateur rejoint un serveur, il doit voir l'historique des messages des canaux texte du serveur, et recevoir en temps réel les nouveaux messages. Aujourd'hui, certains membres n'ont pas de cache de permissions calculées, ce qui casse la diffusion temps réel et masque les erreurs côté client.

Scope

In Scope

  • Diagnostiquer précisément pourquoi un membre récent ne voit pas l'historique.
  • Corriger le flux d'arrivée sur un serveur (join_server) pour que les permissions soient immédiatement disponibles.
  • Réparer les membres existants qui n'ont aucune permission calculée.
  • Rendre visibles les échecs de chargement de l'historique côté frontend.

Out of Scope

  • Changer le modèle de rôles ou de permissions par canal.
  • Ajouter de nouvelles permissions par défaut configurables par l'utilisateur.
  • Refonte du système d'événements de l'EventBus.

User Stories

  • En tant que nouveau membre, je veux voir les anciens messages d'un canal dès que je le rejoins, afin de suivre la conversation.
  • En tant que membre, je veux recevoir les nouveaux messages en direct sans recharger la page.
  • En tant qu'administrateur, je veux que les membres existants soient réparés sans manipulation manuelle de la base.

Functional Requirements

  • Après join_server, un membre standard peut lire (READ_CHANNEL) tous les canaux texte du serveur via GET /messages.
  • Après join_server, le membre reçoit les événements temps réel de messages des canaux du serveur.
  • Les membres existants sans cache (5 sur le serveur principal dans la base locale) sont réparés au démarrage ou par une opération de réparation.
  • Si le chargement de l'historique échoue, l'interface affiche un message d'erreur au lieu d'un canal vide silencieux.

Non-Functional Requirements

  • L'opération de join doit rester transactionnelle et ne pas bloquer la réponse HTTP plus que nécessaire.
  • La réparation doit être idempotente (relançable sans effet de bord).

Technical Design

Current Implementation

Lecture de l'historique (chemin REST)

  • src/routes/message/handlers.rs get_all exige READ_CHANNEL via require_channel_permission.
  • src/http/permissions.rs check_channel_permission (l.192-218) calcule les droits en direct à partir de server_user, role_user, channel_role_permission et channel_user_permission. Pour un canal de serveur, la base de bits part de 0 ; PermissionSet::DEFAULT (src/permissions.rs l.96-105) n'est appliqué qu'aux canaux sans serveur.
  • Le rôle par défaut « Member » reçoit READ_CHANNEL sur chaque canal créé via src/services/channel.rs create_channel (l.69-86). Dans la base locale, chaque rôle par défaut a une permission par canal (11/11 et 4/4), et tous les membres ont ce rôle. Le contrôle REST passe donc pour les membres actuels.

Arrivée d'un membre

  • src/services/server.rs join_server (l.77-137) insère server_user et role_user puis émet ServerUserCreatedEvent après commit. Il n'appelle pas directement la synchronisation du cache.
  • src/services/permission_sync.rs start_listen_event (l.85+) écoute ServerUserCreatedEvent (l.100) et appelle computed_permission.full_sync_user de façon asynchrone.
  • src/repositories/computed_permission.rs full_sync_user (l.108+) recalcule les lignes computed_permission du membre.

Diffusion temps réel

  • src/services/realtime_registry.rs construit l'audience des canaux (refresh_channel l.25-57, initialize l.76+) à partir de la table computed_permission, uniquement pour les lignes portant READ_CHANNEL.
  • src/routes/gateway/mod.rs (l.141-293) envoie tous les événements de messages et de canaux via users_for_channel. Un membre absent de ce registre ne reçoit rien en direct.

Constat dans la base locale (lecture seule, oxspeak.db)

  • Serveur B3D2… (9 membres) : 4 membres ont 12 lignes de cache (11 canaux + niveau serveur), 5 membres ont 0 ligne.
  • Serveur F142… (2 membres) : un membre a 5 lignes, l'autre 0 ligne.
  • Le serveur B3D2… contient des messages (ex. canal Test : 11 messages), donc le problème n'est pas une absence de données.

Affichage côté client

  • frontend/src/stores/message.ts requestPage (l.94-112) lève une erreur sur une réponse non OK, mais fetchMessages (l.148-175) ne fait que console.error. Un 403 apparaît donc comme un canal vide.

Key Decisions

Décision 1 : Source de vérité de l'audience temps réel

  • Option A (recommandée) : garder computed_permission comme cache, mais le rafraîchir de façon synchrone dans join_server (appel à full_sync_user puis realtime_registry.refresh_user) avant de répondre.
  • Option B : rendre l'audience temps réel dépendante de check_channel_permission (plus coûteux, à évaluer sur des serveurs volumineux).

Décision 2 : Réparation des membres existants

  • Option A (recommandée) : au démarrage, lancer full_sync_server pour chaque serveur (idempotent, coût acceptable à faible volume).
  • Option B : une route d'administration ou une commande ponctuelle de réparation.

Décision 3 : Erreur côté frontend

  • Afficher un état d'erreur dans fetchMessages au lieu d'un canal vide. Ce n'est pas une correction de la cause, mais elle rend le problème visible.

Proposed Changes

  • services/server.rs join_server : après commit, synchroniser le cache du membre et rafraîchir le registre temps réel, en conservant l'émission de ServerUserCreatedEvent.
  • services/permission_sync.rs : journaliser les échecs du listener sync_user (aujourd'hui silencieux) pour expliquer les membres restés sans cache.
  • Démarrage (core/mod.rs, après realtime_registry.initialize) : appeler une réparation idempotente full_sync_server pour chaque serveur.
  • frontend/src/stores/message.ts fetchMessages : exposer une erreur de chargement et l'afficher.

Architecture Diagram

graph LR
  J[join_server] --> DB[(server_user, role_user)]
  J --> S[full_sync_user]
  S --> C[(computed_permission)]
  S --> R[realtime_registry]
  R --> G[gateway: message events]
  F[GET /messages] --> P[check_channel_permission]
  P --> DB

Risks

  • Le listener asynchrone peut échouer silencieusement : la cause exacte des 5 membres sans cache doit être confirmée (journaux, rejeu).
  • La réparation au démarrage peut être longue sur de gros serveurs : la limiter aux serveurs dont un membre n'a pas de cache.
  • Le symptôme exact « ne voit pas les anciens messages » n'a pas encore été reproduit à l'interface ; l'historique REST semble autorisé pour les membres actuels. Il faut reproduire avec un compte réellement concerné avant de conclure.
  • Verrou lock_scope de full_sync_user : éviter les appels imbriqués pendant la transaction de join.

Testing

Validation Approach

  • Reproduire le problème par un test d'intégration qui crée un serveur, ajoute un canal avec des messages, fait rejoindre un nouvel utilisateur via join_server, puis vérifie GET /messages et la présence dans le registre temps réel.
  • Utiliser les helpers existants de src/http/test_support.rs et le style des tests de src/routes/channel/tests.rs.

Key Scenarios

  • Un nouvel membre rejoint un serveur ayant des canaux et des messages : GET /messages renvoie les messages, computed_permission contient une ligne READ_CHANNEL par canal.
  • Après join, users_for_channel(channel_id) contient le nouveau membre pour chaque canal texte.
  • La réparation au démarrage ajoute les lignes manquantes pour les 5 membres sans cache et ne modifie pas les autres.
  • Côté frontend, un 403 sur /messages affiche un message d'erreur au lieu d'un canal vide.

Edge Cases

  • Membre qui rejoint un serveur déjà joint (chemin « déjà membre » de join_server) : pas de doublon, cache inchangé.
  • Canal créé avant l'existence du rôle par défaut : vérifier la présence de channel_role_permission.
  • Relance de la réparation : idempotente.
  • Mauvais mot de passe de serveur : aucune ligne créée, aucun événement émis.
  • Régression : les DM (canaux sans serveur) continuent d'utiliser channel_user pour leur audience.

Test Changes

  • Ajouter un test de join dans le module de tests des routes message (à créer à côté de src/routes/message/handlers.rs).
  • Ajouter un test unitaire sur full_sync_server idempotent dans src/repositories/computed_permission.rs.
  • Ne pas modifier les tests existants de realtime_registry.rs.

Delivery Steps

✓ Step 1: Reproduire et confirmer la cause avec un test de join

Un test d'intégration reproduit le cas « nouveau membre sans historique » et confirme la cause côté cache et audience temps réel.

  • Écrire un test qui crée un serveur et un canal texte avec des messages, puis fait rejoindre un nouvel utilisateur via join_server.
  • Vérifier GET /messages (handler get_all dans src/routes/message/handlers.rs) et l'absence éventuelle de lignes computed_permission.
  • Vérifier la présence du membre dans RealtimeRegistry::users_for_channel.
  • Consigner dans les journaux pourquoi le listener sync_user de src/services/permission_sync.rs n'a pas rempli le cache.

✓ Step 2: Synchroniser les permissions dès l'arrivée d'un membre

Un membre qui rejoint un serveur dispose immédiatement de son cache de permissions et de sa place dans le registre temps réel.

  • Dans join_server (src/services/server.rs), après commit, appeler computed_permission.full_sync_user pour le nouveau membre.
  • Appeler realtime_registry.refresh_user pour mettre à jour les canaux de l'utilisateur.
  • Garder l'émission de ServerUserCreatedEvent pour les autres abonnés.
  • Journaliser l'échec de synchronisation au lieu de l'ignorer dans le listener sync_user.

✓ Step 3: Réparer les membres existants au démarrage

Les membres déjà présents sans cache de permissions récupèrent leurs lignes au prochain démarrage du serveur.

  • Ajouter une réparation idempotente qui appelle full_sync_server pour chaque serveur dont un membre n'a pas de ligne computed_permission.
  • Brancher la réparation dans src/core/mod.rs après realtime_registry.initialize.
  • Ajouter un test unitaire vérifiant que deux exécutions successives ne modifient pas les lignes.

✓ Step 4: Afficher les erreurs de chargement de l'historique dans le frontend

Un échec de chargement de l'historique est visible par l'utilisateur au lieu d'un canal vide silencieux.

  • Dans frontend/src/stores/message.ts, fetchMessages expose l'état d'erreur (par exemple loadError) au lieu de seulement console.error.
  • Afficher un message d'erreur avec un bouton de nouvel essai dans le composant de liste de messages.
  • Ne pas modifier le comportement de fetchBefore et fetchAfter.