Skip to content

feat(admin): construire la file de modération et ses décisions - #365

Draft
Nascimento95 wants to merge 23 commits into
issue/52-admin-foundationfrom
issue/53-moderation-queue
Draft

Nascimento95 wants to merge 23 commits into
issue/52-admin-foundationfrom
issue/53-moderation-queue

Conversation

@Nascimento95

@Nascimento95 Nascimento95 commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Résultat et problème

L'équipe n'avait aucun moyen de relire un parcours soumis. Cette PR ajoute la file de modération (A-02) : la liste, la fiche d'un parcours et les décisions, côté API et côté admin.

PR en brouillon : deux critères d'acceptation sur quatre sont tenus ; les deux autres dépendent de données qui n'existent pas encore dans l'API (voir « Limites »).

Ce qui est fait :

  • packages/shared : les 7 motifs de refus et leurs libellés français et anglais, les schémas de la file, de la fiche et des décisions, le code already_decided.
  • apps/api : table moderation_actions ; GET /admin/moderation (filtres statut, ville, retard, révision ; recherche ; tri ; pages) ; GET /admin/routes/:id (fiche, auteur, historique, place dans la file) ; publish, request-changes, reject, unpublish, chacune écrite dans moderation_actions et admin_actions dans une seule transaction ; 409 already_decided avec la décision prise quand un autre membre a tranché.
  • apps/admin : la file (ancienneté, retard en rouge avec icône et texte lu, pastille « À corriger », filtres, tri et recherche gardés dans l'adresse, états vide, chargement, erreur) ; la fiche en page dédiée (« Parcours 1 sur 14 », avertissements, parcours tel qu'il sera publié, plan et étapes, auteur, historique) ; la charte à cocher, « Publier » actif quand les 6 règles sont cochées, « Demander des modifications » et « Refuser » avec leurs fenêtres, toasts, passage au parcours suivant.
  • packages/ui : champ de recherche, filtre à menu, case à cocher, avatar à initiale, confirmation non destructive.

Limites :

Fixes #53
Écrans : A-02
Dépendance : empilée sur #363 (socle admin, #52).

Comment vérifier

Montage local de #363, puis passer des parcours en attente dans la base locale (update routes set status = 'pending' where …) et se connecter avec un compte moderator ou admin.

Testé par Marco le 2026-10-12, en local avec l'émulateur Auth, face aux planches A-02 du wiki :

  • la file : 4 parcours, le plus ancien en tête, « 3 j » en rouge, « À corriger », « Première soumission », « 1 à 4 sur 4 » ;
  • les filtres « Plus de 72 h », « Révisions » (liste vide) et « Ville », le tri par colonne et la recherche, avec l'adresse qui suit ;
  • la fiche : avertissements, plan et étapes, « Lieu jamais vérifié », carte de l'auteur ;
  • « Publier » grisé jusqu'aux 6 règles cochées ; « Demander des modifications » bloqué sans message ; toast et passage au parcours suivant ;
  • le rendu de « Révision », du sceau certifié et du bloc des changements, sur des valeurs forcées en local puis retirées.

Vérifié en base locale après ces décisions : une ligne dans moderation_actions et une dans admin_actions par décision, avec l'état avant et après.

Non vérifié à l'écran : la fenêtre « Refuser » jusqu'à l'envoi, et un parcours rouvert après décision (couverts par les tests de l'API). Rien n'a été testé contre un environnement déployé.

  • pnpm lint && pnpm typecheck && pnpm test verts en local
  • Testé sur iOS / Android (préciser) — sans objet

Formatage mécanique : non

Risque

Niveau : 2 — migration additive (moderation_actions et son type énuméré), nouveaux endpoints authentifiés, règle de modération.

Taille : deux commits juste au-dessus de 400 lignes utiles. 82fcd73 (419) : les composants de la liste, son conteneur et ses textes. 0ad9a3f (415) : la colonne de décision, ses deux fenêtres, son hook et le conteneur de la fiche. Fichiers générés à part : migration 0016_add_moderation_actions.sql et son instantané.

Revue OWASP (Top 10) :

  • Contrôle d'accès : toutes les routes passent par requireAdminSection('moderation') ; un éditeur reçoit 403 sur la file, la fiche et les décisions, sans rien écrire (testé).
  • Intégrité et concurrence : une décision verrouille la ligne du parcours (select … for update), vérifie son statut, puis écrit le statut et les deux journaux dans la même transaction ; deux décisions simultanées donnent un 200 et un 409, une seule ligne dans chaque journal (testé avec deux requêtes en parallèle).
  • Injection : requêtes paramétrées ; % et _ de la recherche sont échappés ; filtres et tri validés par un schéma strict, un filtre inconnu est refusé (400).
  • Entrées non fiables : le message à l'auteur est du texte brut borné à 1 000 caractères, sans balise ni caractère de contrôle hors saut de ligne ; le motif est une valeur de la taxonomie ; l'admin affiche ces textes sans HTML.
  • Journalisation : chaque décision garde l'auteur, l'action, l'état avant et après, le motif et le message.
  • Configuration : img-src https: ajouté à la CSP de l'admin (voir Limites).

Nouvelle dépendance : aucune.

Retour arrière

Le revert de la PR suffit pour le code. La migration 0016 est additive : après un revert, la table moderation_actions et le type moderation_action restent en base sans effet ; un parcours déjà publié, refusé ou dépublié par une décision garde son statut.

<img width="736" height="852" alt="Capture d’écran 2569-10-13 à 13 22 12" src="https://github.com/user-attachments/assets/07800634-b1f8-4163-a883-9f1d3cd85360"
Capture d’écran 2569-10-13 à 13 13 17
/>
Capture d’écran 2569-10-13 à 13 22 42
Capture d’écran 2569-10-13 à 13 15 09
Capture d’écran 2569-10-13 à 13 13 45

@IlanAMG

IlanAMG commented Oct 13, 2026

Copy link
Copy Markdown
Member

Revue du 13 octobre 2026 — session d'Ilan, relecture par un agent du code à la tête a1931e0. Rien n'a été lancé pour cette relecture ; Ilan fait sa recette visuelle en local à part.

Solide : rôle vérifié par l'API sur toutes les routes (éditeur refusé), décision prise sous verrou dans une transaction (409 sur un parcours déjà décidé), entrées validées, migration additive, recherche de l'app limitée à published.

À corriger avant la sortie de brouillon

  1. Textes qui annoncent une notification qui n'existe pas : apps/admin/src/messages/fr.ts, lignes 301, 303 et 309 (« L'auteur reçoit le motif et votre message… », « L'auteur le lit dans l'app. ») et le toast « Demande envoyée ». Rien n'est envoyé à l'auteur.
  2. Migration : même piège que le socle (feat(admin): poser le socle, la connexion, les rôles et le journal #363). 0004_add_moderation_actions (when 1791798381842) est antérieure à 0003_add_direction_cache de main : à régénérer après le rebase, pas à renommer.
  3. Un parcours « À corriger » passe en rouge « en retard » : moderation/queue.ts compte le retard sur les deux statuts, alors que le compteur de la navigation ne compte que pending (admin/counters.ts).
  4. Message refusé sans explication s'il contient <, > ou une tabulation : le schéma le rejette et le toast dit seulement « La décision n'a pas pu être enregistrée. » (useModerationDecision.ts).
  5. Compteur du menu : rafraîchi toutes les 60 s seulement, pas après une décision.

Écarts avec les planches A-02

  • Après « Publier », l'écran saute au parcours suivant ; la planche montre un état « Publié à l'instant ».
  • « Refuser » : liste déroulante au lieu des 7 motifs avec leur explication, sans compteur de caractères.
  • À 1 024 px, la colonne Ville reste.

En attente d'une décision d'Ilan

Relevé aussi, mineur : l'ancienneté repart de zéro à chaque changement de updated_at (limite déjà annoncée) ; moderation/detail.ts sert aussi un brouillon ou un parcours privé à qui connaît son identifiant ; onDelete: 'cascade' efface l'historique de modération d'un parcours supprimé ; img-src https: dans la CSP de l'admin.

@Nascimento95
Nascimento95 force-pushed the issue/52-admin-foundation branch from 3bdf63e to 03d6216 Compare October 14, 2026 04:23
@Nascimento95
Nascimento95 force-pushed the issue/53-moderation-queue branch from a1931e0 to 1386f8a Compare October 14, 2026 04:26
@Nascimento95

Copy link
Copy Markdown
Collaborator Author

Branche rebasée sur le socle mis à jour (#363, tête 03d6216), donc sur main : la migration de la modération est régénérée par db:generate et devient 0006_add_moderation_actions, après 0005_add_admin_actions ; les when du journal sont croissants. Aucun autre changement de code. pnpm lint && pnpm typecheck && pnpm test verts en local (431 tests API, dont la garde de toutes les routes /admin/*, 95 admin). Les autres points de la revue du 13 octobre restent à traiter.

@Nascimento95

Copy link
Copy Markdown
Collaborator Author

Quatre points de la revue du 13 octobre corrigés (0fb7169, 985f2af) :

  • Faux retard : seul un parcours pending est en retard, dans la file, le filtre « Plus de 72 h », le compteur de la file et l'en-tête de la fiche ; test d'API ajouté.
  • Textes : les fenêtres et le toast ne promettent plus d'envoi à l'auteur (« gardé dans l'historique ; l'auteur n'est pas encore averti automatiquement »).
  • Message refusé : un validation_error affiche « texte brut uniquement, sans < ni >, ni tabulation, 1 000 caractères au plus », avec un indice sous le champ.
  • Compteur du menu : relu à chaque changement de page, donc juste après une décision.

Vérification humaine : Marco a testé le 2026-10-14 en local (file à trois parcours, dont un « À corriger » de 4 jours non rouge, filtre « Plus de 72 h », message avec < refusé et expliqué, demande de modifications, refus, compteur du menu).

Restent : les écarts avec A-02 (état « Publié à l'instant », les 7 motifs au refus, colonne Ville à 1 024 px) et les trois décisions d'Ilan.

@Nascimento95

Copy link
Copy Markdown
Collaborator Author

Les trois écarts avec les planches A-02 relevés dans la revue du 13 octobre sont corrigés :

  • Publié à l'instant : après « Publier », la fiche reste ; la colonne de décision affiche « Publié à l'instant », « par {prénom}, les 6 règles vérifiées », puis « Parcours suivant » et « Voir dans Lieux et parcours » ; le compteur du menu est relu aussitôt. La phrase « {prénom} est prévenue » de la planche n'est pas reprise : aucune notification n'existe encore.
  • Refuser : les 7 motifs en deux colonnes avec leur explication, « Message à {prénom} », erreur « Écrivez un message à l'auteur. » dans le champ, sans compteur de caractères. Pour une révision, le texte dit que la version publiée reste en ligne (non testable : aucune révision n'existe encore). Composant RadioGroup ajouté à packages/ui.
  • 1 024 px : sous 1 280 px la colonne Ville disparaît et la ville passe sous le titre (« Paris, première soumission »).

Vérification humaine : Marco a testé le 2026-10-14 en local, face aux planches A-02 (refus, publié, liste à 1 024 px) : publication et panneau « Publié à l'instant », refus avec motifs et message vide, colonne Ville selon la largeur. Lint, types et tests verts en local (95 tests admin, 37 UI).

Restent les trois décisions d'Ilan : notification à l'auteur, « Premium » à la publication, confirmation avant « Publier ».

@Nascimento95

Copy link
Copy Markdown
Collaborator Author

Branche remise à jour sur le socle rebasé sur main (#363, tête a850abb) : la migration de la modération est régénérée par db:generate en 0015_add_moderation_actions, après 0014_add_admin_actions ; when du journal croissants, base de test recréée de zéro.

Adaptations au rebase : le schéma, httpError (avec details pour already_decided) et le client API reprennent ceux de main ; moderation_actions.actor_id est classé dans le test d'export des données ; les tests des corps invalides attendent 422, la convention de main (D-071), au lieu de 400. Lint, types et tests verts en local.

…on queue

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Nascimento95
Nascimento95 force-pushed the issue/52-admin-foundation branch from a850abb to b181be3 Compare October 16, 2026 03:33
Nascimento95 and others added 10 commits October 16, 2026 10:33
…rrors

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rch (A-02)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s of a revision

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…story and queue place

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nascimento95 and others added 12 commits October 16, 2026 10:33
…confirmation

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e and refresh the menu counter

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…-02)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…er the title below 1280 px (A-02)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@Nascimento95
Nascimento95 force-pushed the issue/53-moderation-queue branch from 5282768 to 6135674 Compare October 16, 2026 03:34
@Nascimento95

Copy link
Copy Markdown
Collaborator Author

Branche remise à jour sur le socle rebasé (#363) : la migration de la modération est régénérée en 0016_add_moderation_actions, après 0015_add_admin_actions ; when du journal croissants. Un test du retard est rendu indépendant des autres fichiers de test (le compteur couvre toute la file : « au moins les nôtres »). Lint, types et tests verts en local.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

level:2 Risque niveau 2 (SECURITY.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants