Skip to content

feat(admin): #128 — unités encadrées à l'écran et dans la visibilité d'un chef - #185

Merged
mathiscapart merged 2 commits into
developfrom
feat/unitlead-ecrans
Oct 8, 2026
Merged

mathiscapart merged 2 commits into
developfrom
feat/unitlead-ecrans

Conversation

@mathiscapart

Copy link
Copy Markdown
Owner

Origine

Refs #128 — étape 4 sur 4 (écrans et visibilité). Je ne ferme pas l'issue : la reprise de données (« saisir les compagnons qui encadrent déjà ») se fait à la main, avec l'écran ajouté ici.

Ce qui change

  • Un chef voit aussi les contenus des unités qu'il encadre. Il accède à leurs salons, reçoit leurs annonces et leurs notifications d'événement, et retrouve leurs événements dans son agenda iCal. Lucas, compagnon et chef des Louveteaux, voit le salon des Louveteaux. Robin, compagnon simple, ne le voit pas.
  • Exclusion d'un salon : un compte n'est exclu que si toutes ses unités (appartenance + encadrées) sont exclues. Sans aucune unité, rien ne change. L'ADMIN passe toujours.
  • Nouvel éditeur « Unités encadrées » sur /admin/utilisateurs/[id]/modifier, à côté de l'unité d'appartenance. Pour un compte qui n'est pas Chef, l'éditeur explique qu'il faut d'abord lui donner ce rôle. Si le rôle a été retiré entre-temps, le serveur refuse avec « Seul un compte Chef peut encadrer une unité. ».
  • Les unités encadrées s'affichent dans la liste admin, avec un filtre « Encadre », et sur la fiche membre. Le journal d'audit trace l'avant et l'après.
  • Le bilan des présences présélectionne la première unité encadrée.

Pourquoi

Après #183 et #184, Lucas pouvait agir sur les Louveteaux sans voir leurs salons, ni leurs annonces, ni leurs événements. Aucun écran ne permettait non plus de dire quelles unités un chef encadre : setUserLeadUnits n'avait pas d'appelant.

Preuves

  • pnpm lint : ✅ 0 erreur. Les 3 warnings existent déjà sur develop (TicketsSection.tsx, place-actions.ts).
  • pnpm typecheck : ✅
  • pnpm test : ✅ 627 tests, contre 602 sur develop. Les 25 tests ajoutés ont été écrits avant le code : 10 échecs constatés pour la bonne raison (fonction absente, unité encadrée ignorée), puis verts.
    • src/modules/communication/access.test.ts (nouveau, 16 tests) :
      • Lucas voit le salon des Louveteaux, Robin non ;
      • un CHEF garde le salon de son unité d'appartenance ;
      • un UnitLead sans rôle Chef n'ouvre rien ;
      • un CHEF sans UnitLead garde le comportement d'avant ;
      • un compte suspendu est refusé ;
      • règle d'exclusion « toutes les unités » ;
      • un compte sans unité n'est jamais exclu ;
      • l'ADMIN passe toujours ;
      • écriture refusée dans un salon archivé ;
      • un chef actuel garde exactement ses accès : pour 96 combinaisons de salon et pour 3 unités, canAccessChannel et canWriteChannel donnent le même résultat avec UnitLead = unité qu'avant refactor(comptes): un compagnon peut être chef — le modèle ne gère ni plusieurs rôles d'unité ni plusieurs unités #128.
    • src/modules/communication/audience.test.ts (+5) :
      • un chef reçoit les annonces des unités qu'il encadre et garde son unité d'appartenance ;
      • un UnitLead sans Chef n'ajoute rien ;
      • ses propres parents n'entrent pas dans l'audience ;
      • un chef repris (UnitLead = unité) garde la même audience sur toutes les cibles.
    • src/lib/permissions.test.ts (+3) : ledUnitsOf.
    • src/modules/admin/user-filters.test.ts (+1) : filtre ledUnit en liste blanche.
    • Aucune ligne de test existante n'a été modifiée ni supprimée : le diff des fichiers de test ne contient que des ajouts.
  • Parcours réel : ✅ Playwright (Chromium) sur pnpm dev:worktree (port 3101), base du worktree seedée (db:seed puis db:seed:branches). Chaque vérification lit aussi l'état en base. Après les correctifs de relecture, 29/29 et 21/21 vérifications vertes.
    • Données hors seed, insérées en base pour le parcours. L'app n'a aucun écran de création de salon et le seed n'a pas de salon Louveteaux. J'ai donc inséré deux salons : louveteaux (accessUnits ["LOUVETEAUX"]) et tous-sauf-compagnons (ouvert, excludeUnits ["COMPAGNONS"]).
    • Visibilité :
      • Lucas (CHEF, 17 ans, membre des Compagnons, encadre les Louveteaux) :
        • il voit louveteaux, compagnons et tous-sauf-compagnons, pas pionniers ;
        • il écrit dans louveteaux (message en base) ;
        • il est notifié de l'annonce Louveteaux publiée par l'admin et la voit dans /annonces ;
        • quand Timothée, chef des Louveteaux, crée « Grand jeu Louveteaux », Lucas reçoit EVENT_UPDATE. Léa (Louveteaux) le reçoit aussi, comme avant. Robin et Éléonore (cheffe des Compagnons) ne le reçoivent pas. L'événement est posté dans le salon louveteaux ;
        • son iCal (jeton créé par /compte) contient les événements Louveteaux et ceux des Compagnons. Celui de Robin ne contient aucun événement Louveteaux ;
        • /planning/presences ne propose que les Louveteaux, présélectionnés ;
        • il pointe un Louveteau présent. Le pointage d'un événement Compagnons le redirige ;
        • sur la progression d'une Louveteau, il a les notes de suivi ; sur celle de Robin, il ne les a pas.
      • Robin (compagnon simple) n'a rien de plus :
        • /communication/louveteaux → 404 ;
        • tous-sauf-compagnons absent de sa liste ;
        • /planning/presences → redirection vers /dashboard.
      • Thomas Martin (chef actuel des Pionniers) : il voit pionniers et pas louveteaux, comme avant.
    • Écran admin (setUserLeadUnits, première exécution réelle) :
      • l'admin ajoute les Scouts-Guides à Lucas. En base, UnitLead = [LOUVETEAUX, SCOUTS]. L'audit porte previousUnits ["LOUVETEAUX"] → units ["LOUVETEAUX","SCOUTS"], avec l'admin pour auteur. Lucas voit alors scouts-guides et son bilan propose les deux unités ;
      • l'admin retire les Scouts-Guides. En base, [LOUVETEAUX], avec l'audit inverse. Lucas perd scouts-guides ;
      • la liste filtrée « Encadre : Louveteaux » renvoie Lucas et Timothée. La colonne affiche « Encadre : LOUVETEAUX, SCOUTS » sous COMPAGNONS ;
      • la fiche membre de Lucas affiche « Encadre : Louveteaux-Jeannettes (8-11 ans), Scouts-Guides (11-14 ans) » ;
      • Robin, qui n'est pas Chef : l'éditeur affiche « Seul un compte Chef peut encadrer une unité. … », sans bouton Enregistrer ;
      • refus serveur : une page reste ouverte sur Marion (Chef + Trésorière). Dans un autre onglet, l'admin lui retire le rôle Chef, ce qui retire son encadrement. Depuis la page restée ouverte, il enregistre ses unités encadrées. Le toast « Seul un compte Chef peut encadrer une unité. » s'affiche ; aucune ligne n'est écrite et aucun audit n'est créé. Marion retrouve ensuite le rôle Chef, et donc les Farfadets.
    • Non vérifiés : l'envoi d'emails et de push (seule la table Notification est contrôlée), et le flux SSE en direct.
    • Captures jointes en commentaire, avec des données du seed uniquement.
  • Agents utilisés (lancés en parallèle sur le commit 92c4c8a) :
    • security-auditor : 1 passe. Aucun point bloquant ni élevé. 4 points relevés, 0 corrigé, 4 laissés (voir « Risques ») :
      • MOYEN : le choix A ouvre désormais aussi les salons ;
      • FAIBLE : le flux SSE n'est pas réévalué après une révocation ;
      • FAIBLE : leaderIds ignore canLogin ;
      • FAIBLE : la construction de ledUnits est dupliquée.
    • reviewer : 1 passe. Il a atteint sa limite de tours et je l'ai relancé une fois pour qu'il conclue : c'est la même relecture. Il n'a pas relu les tests, CLAUDE.md, D-045 ni la fin de user-actions.tsx. Aucun point bloquant ni élevé. 5 points relevés :
      • corrigés (2) : l'unité par défaut des présences dépendait de l'ordre de chargement des UnitLead, et le libellé « Encadre » était calculé deux fois par ligne ;
      • laissés (3) : MOYEN, l'élargissement concerne aussi le signalement et les sondages (voir « Risques ») ; FAIBLE, libellés d'unité hétérogènes ; FAIBLE, requête leaderIds faite aussi pour les appelants qui ne s'en servent pas.
      • Il a confirmé le raisonnement sur le tableau de bord et le respect des interdits.
    • verifier : 1 passe sur le commit 92c4c8a. Il a rejoué les deux scripts (29/29, 21/21) et vérifié par ses propres scripts :
      • Thomas, chef actuel, est notifié d'un événement Pionniers créé par Antoine ; Lucas non ;
      • Lucas lit et écrit dans tous-sauf-compagnons, Robin reçoit 404 ;
      • l'admin retire toutes les unités encadrées de Lucas par l'écran : Lucas reçoit 404 sur louveteaux et /planning/presences le renvoie vers /planning. Les unités sont rendues ensuite, avec un audit à chaque étape.
      • Son nettoyage a supprimé ses propres données (1 événement, 1 message et 72 notifications liées à cet événement).
    • Les 2 correctifs n'ont pas eu de seconde passe d'agent. Ils sont couverts par lint, typecheck, les 627 tests et le rejeu des deux scripts (29/29, 21/21).
    • Non lancés :
      • architect : conception tranchée par Mathis (option E et règles de la tâche) ;
      • feature et test-engineer : code et tests écrits dans le fil principal, tests rouges avant le code ;
      • debugger : aucun bug à diagnostiquer ;
      • refactorer : pas de refactor ;
      • doc-writer : CLAUDE.md et D-045 rédigés dans le fil principal ;
      • git-manager : commits et PR faits dans le fil principal ;
      • explorer / Explore : recherches faites dans le fil principal ;
      • project-manager : sans objet.

Invariants

  • Mutations sous withAudit() : la seule mutation touchée est setUserLeadUnits, déjà sous withAudit. Son audit porte maintenant previousUnits, lu dans la même transaction. Le refus pour un compte non Chef annule la transaction sans écrire d'audit.
  • Actions/pages sensibles gardées par can(user, "…") :
    • l'écran est sur une page requireCan("user.manage") ;
    • l'action passe par ensureCan("user.manage") puis assertCanManageTarget (garde inchangée) ;
    • la matrice PERMISSIONS et inUnitScope ne sont pas modifiées ; seul le helper ledUnitsOf est ajouté.
  • Migration Prisma : Sans objet.
  • CSP Traefik mise à jour si nouveau domaine externe : Sans objet.
  • Données de mineurs / RGPD touchées :
    • un chef encadrant, éventuellement mineur comme Lucas, lit désormais les salons des unités qu'il encadre et reçoit leurs annonces et leurs événements. C'est la décision « les unités encadrées donnent aussi la visibilité » ;
    • SAFE-01 et dm-policy sont inchangés : Lucas n'écrit toujours en privé à aucun mineur ;
    • les parents d'un chef encadrant n'entrent pas dans l'audience des unités qu'il encadre.

Décisions prises

  1. Une unité encadrée ne compte que pour un compte CHEF (ledUnitsOf), comme dans inUnitScope et dm-policy. Une ligne UnitLead orpheline n'ouvre rien.
  2. Tableau de bord non modifié. Tout CHEF a event.manage (PERMISSIONS["event.manage"] = [CHEF, RG]), et pour lui getNextEvent ne filtre pas par unité : il voit déjà les événements des Louveteaux. Ajouter les unités encadrées au filtre « membre » aurait été du code mort.
  3. Notification d'événement : leaderIds est un champ à part de resolveUnitAudience, hors allIds. Seul notifyEventAudience l'utilise. Les campagnes de cotisation (jeunes et parents) et les messages de prêt et de tâche ne changent pas. reminders.ts reste sur l'appartenance, selon la consigne.
  4. Bilan des présences : l'unité présélectionnée est la première unité encadrée dans l'ordre du catalogue, sinon l'unité d'appartenance. Pour tout chef actuel, le résultat ne change pas.
  5. Écran : pour un compte non Chef, l'éditeur explique la règle au lieu de proposer des cases que le serveur refuserait. Le dialogue « Unité » s'intitule désormais « Unité d'appartenance ».
  6. Audit lisible : USER_LEAD_UNITS_CHANGED porte l'avant et l'après, comme la correction de date de naissance.
  7. CLAUDE.md gagne la convention « unité d'appartenance ≠ unités encadrées ». D-045 est amendée (étape 4).

Risques et points à vérifier

  • À trancher par Mathis (MOYEN, security-auditor) : effet du choix A. Un RG ou une secrétaire qui s'attribue CHEF puis des unités encadrées lit et écrit dans les salons de ces unités et reçoit leurs annonces et leurs événements. Avant cette PR, cela ne lui donnait que le périmètre d'action. Tout est tracé (USER_ROLE_CHANGED, USER_LEAD_UNITS_CHANGED, où l'acteur est la cible), mais personne n'est prévenu. Correctif proposé, non appliqué puisque le choix A est validé : notifier l'ADMIN et le RG quand un compte modifie ses propres unités encadrées.
  • MOYEN (reviewer) : l'accès élargi vaut partout où canAccessChannel est appelé. C'est le cas du signalement d'un message (moderation-actions.ts:66, il faut voir le salon pour signaler) et des sondages. Lucas peut donc signaler un message du salon des Louveteaux et y voter. Le traitement des signalements passe toujours par canModerateReport et ledUnits (feat(communication): #128 — chef d'unité par UnitLead en messagerie, modération et pédagogie #184).
  • FAIBLE (security-auditor), déjà vrai avant : un flux SSE ouvert n'est pas réévalué. Après le retrait d'une unité, le flux continue d'envoyer des événements {type, id} jusqu'à la reconnexion. Le contenu, lui, passe par des actions qui revérifient l'accès.
  • FAIBLE (security-auditor) : leaderIds ne filtre pas canLogin, comme memberIds aujourd'hui. Un compte enfant porteur de CHEF, cas qui n'existe pas, serait notifié.
  • FAIBLE :
    • unitLeads → ledUnits est construit à 4 endroits nouveaux (SSE, notifyChannelMessage, loadAudienceContext, iCal). Si un futur appelant l'oublie, il est fail-closed, sans fuite ;
    • la requête leaderIds s'exécute aussi pour les appelants de resolveUnitAudience qui ne s'en servent pas ;
    • les libellés d'unité ne sont pas uniformes : codes bruts dans la liste admin, comme la colonne Unité juste au-dessus ; libellé long sur la fiche ; libellé court sur le bouton.
  • À relire en priorité : communication/access.ts (règle d'exclusion), communication/audience.ts, audience/unit-audience.ts et LeadUnitsEditor dans user-actions.tsx.
  • La base de recette n'a pas de salon Louveteaux. Pour voir l'effet en vrai, il faut un salon dont accessUnits = ["LOUVETEAUX"].

🤖 Generated with Claude Code

mathiscapart and others added 2 commits October 8, 2026 15:21
…d'un chef

- Salons : un chef accède aux salons des unités qu'il encadre ; exclusion
  (excludeUnits) seulement si toutes ses unités sont exclues.
- Annonces, notifications d'événement et iCal : un chef reçoit aussi ceux
  des unités qu'il encadre. Relances et cotisations restent à l'appartenance.
- Bilan des présences : unité présélectionnée parmi les unités encadrées.
- /admin/utilisateurs/[id]/modifier : éditeur « Unités encadrées »
  (setUserLeadUnits), message clair pour un compte non Chef ; audit avec
  l'avant et l'après. Unités encadrées affichées dans la liste (avec filtre)
  et sur la fiche membre.
- CLAUDE.md : unité d'appartenance ≠ unités encadrées. D-045 amendée.

Refs #128

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alogue

Relecture : la première unité encadrée dépendait de l'ordre de chargement
des UnitLead. Libellé « Encadre : … » calculé une fois par ligne.

Refs #128

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mathiscapart

Copy link
Copy Markdown
Owner Author

Captures du parcours (données du seed uniquement)

Les salons louveteaux et tous-sauf-compagnons ont été insérés en base pour le parcours : l'app n'a pas d'écran de création de salon.

Visibilité : Lucas (compagnon, chef des Louveteaux) face à Robin (compagnon simple)

1. Lucas voit le salon des Louveteaux. Dans « Unités », Lucas voit louveteaux-jeannettes, l'unité qu'il encadre, compagnons, son unité d'appartenance, et tous-sauf-compagnons. Il n'est pas exclu de ce dernier, puisque l'une de ses unités, les Louveteaux, n'est pas exclue.
Lucas voit le salon des Louveteaux

2. Robin ne voit que compagnons. Pas de salon Louveteaux. tous-sauf-compagnons est absent aussi : sa seule unité est exclue, l'absence est voulue.
Robin ne voit que compagnons

3. Robin reçoit un 404 sur /communication/louveteaux. Ce 404 est voulu : un salon inaccessible ne se distingue pas d'un salon inexistant. C'est le contraste avec la capture 1.
404 voulu pour Robin

4. Lucas voit l'annonce adressée aux Louveteaux. En base, il en a aussi reçu la notification ; Robin ne l'a pas reçue.
Lucas voit l'annonce Louveteaux

5. La cloche de Lucas. Elle affiche « Nouvel événement : Grand jeu Louveteaux », créé par le chef des Louveteaux, et l'annonce Louveteaux. Avant cette PR, Lucas n'était pas notifié des événements des Louveteaux. Les emojis s'affichent en carrés : il manque la police dans le Chromium headless.
Notifications de Lucas

6. Bilan des présences de Lucas. Seuls les Louveteaux sont proposés, et ils sont présélectionnés.
Bilan des présences

7. Lucas pointe un Louveteau présent sur l'événement Louveteaux. Sur un événement des Compagnons, il est redirigé, et c'est voulu : il y est jeune, pas chef.
Pointage Louveteaux

8. Progression d'une Louveteau. Lucas a les actions et les notes de suivi. Sur la progression de Robin, il ne les a pas.
Progression d'une Louveteau

Écran d'administration

10. Fiche de modification de Lucas. Les boutons « Compagnons » (unité d'appartenance) et « Encadre : Louveteaux-Jeannettes » (unités encadrées) sont côte à côte.
Modifier Lucas

11. Dialogue « Unités encadrées ». L'admin ajoute les Scouts-Guides.
Dialogue unités encadrées

12. Après l'enregistrement. Le toast de succès s'affiche. La page était encore en cours de rafraîchissement au moment de la capture, d'où l'ancien libellé du bouton. Le libellé à jour a été vérifié ensuite par le script.
Après ajout

13. Liste filtrée sur « Encadre : Louveteaux-Jeannettes ». Elle renvoie Lucas et Timothée. Sous l'unité d'appartenance, on lit « Encadre : LOUVETEAUX, SCOUTS ».
Liste filtrée

14. Fiche membre de Lucas. Elle affiche « Encadre : Louveteaux-Jeannettes (8-11 ans), Scouts-Guides (11-14 ans) ».
Fiche membre

15. Robin n'est pas Chef. L'éditeur explique qu'il faut d'abord le rôle Chef, et n'affiche pas de bouton Enregistrer. Ce refus est voulu : une unité encadrée n'a de sens que pour un chef.
Compte non Chef

16. Refus côté serveur. Cette page est restée ouverte pendant qu'un autre onglet retirait le rôle Chef à Marion. L'enregistrement de ses unités encadrées est refusé avec « Seul un compte Chef peut encadrer une unité. », et rien n'est écrit. Le bouton « Encadre : Farfadets » est l'état périmé de la page, pas l'état en base. Le refus est voulu.
Refus serveur

17. Journal d'audit, « Unités encadrées modifiées ». Chaque entrée porte l'avant (previousUnits) et l'après (units). On y voit l'ajout puis le retrait des Scouts-Guides.
Audit

@mathiscapart
mathiscapart merged commit 66dc9f0 into develop Oct 8, 2026
4 checks passed
@mathiscapart
mathiscapart deleted the feat/unitlead-ecrans branch October 8, 2026 15:27
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