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

155 lines
11 KiB
Markdown

---
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
```mermaid
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`.