Durée de la simulation 00:00:00

Contexte de l'exercice

Informations clés pour comprendre le cadre de la simulation.

-
-
    Twister 0
    Suggestions
    Médias Presse (web) 0
    Suggestions

    Contexte de l'exercice

    Saisissez ou modifiez les informations clés du scénario.

    0 / 500
    0 / 500
    0 / 3000
      Éditer un inject
      1Contenu
      2Paramètres
      3Publication
      CANAL DE DIFFUSION

      Rédigez le message qui sera publié.

      0 / 280
      APERÇU TWISTER

      Ceci est un aperçu. Le rendu final peut varier selon le type d'appareil.

      Conseils
      • Restez factuel et crédible
      • Adaptez le ton au contexte
      • Privilégiez les messages courts et clairs
      • Pensez à l'impact attendu
      TWISTER – VIRALITÉ & MÉTRIQUES

      Ces valeurs pourront être ajustées après publication.

      Aucun paramètre supplémentaire pour ce canal.

      PROGRAMMATION
      Publier maintenant
      L'inject sera diffusé immédiatement.
      Programmer pour plus tard
      Choisissez une date et une heure de diffusion.
      DIFFUSION IMMÉDIATE
      L'inject sera envoyé à tous les participants dès validation.
      DIFFUSION PROGRAMMÉE
      T + min après le début de l'exercice
      RÉSUMÉ DE L'INJECT

      En publiant cet inject, tous les participants le recevront dans leur environnement de simulation.

      Injects suggérés
      Timeline des publications
      Aucune publication
      IMPORT EN COURS Préparation de l'exercice…
      1. Lecture du fichier Excel
      2. Analyse des injects du chronogramme
      3. Génération du contexte d'exercice (IA)
      4. Création de la couverture médiatique (IA)
      5. Sauvegarde du scénario
      Le traitement peut prendre 30 à 60 secondes selon la taille du chronogramme.
      IMPORT Chronogramme importé
      - Inject envoyé
      T+0 min -
      Émetteur-
      Récepteur-
      Résumé
      Stimuli - message à délivrer
      Réaction attendue
      Commentaire animateur
      TWISTER
      En attente de publications…
      Récents
      En attente…
      MÉDIA PRESSE Flux web d'actualité simulés
      En attente de publications…
      INDICATEURS DE RÉPUTATION
      TONALITÉ MÉDIATIQUE
      -
      -
      -1000+100
      -
      VOLUME DE MENTIONS
      -
      -
      vs période précédente
      NIVEAU DE RISQUE
      -
      Risque réputationnel
      -
      RÉPARTITION DES SENTIMENTS
      😟 Négatif
      -
      😐 Neutre
      -
      😊 Positif
      -
      Sur 0 mention
      Simulation en pause
      L'animateur a suspendu l'exercice. Veuillez patienter.
      OBSERVATEURS 0
      Messages envoyés par les observateurs. Répondez directement ici.
      Aucun message des observateurs.
      Salles actives
      -
      salles en ligne
      Joueurs connectés
      -
      participants actifs
      Prochaine étape
      -
      -
      Injects restants
      -
      à venir
      Déroulé de l'exercice
      Chargement…
      Mes observations
      ÉQUIPE D'ANIMATION 0
      Messages visibles uniquement par l'équipe d'animation. Utilisez ce canal pour partager vos observations ou demander des ajustements.
      Aucun message. Démarrez la conversation avec les animateurs.
      Configuration
      Paramètres

      État du système

      Vue d'ensemble de la connectivité et de l'activité du système.

      APIs externes
      Twister API v2
      Connexion active
      Non configuré
      AI
      Anthropic Claude API
      Connexion active
      Non configuré
      Toutes les API sont opérationnelles.
      Activité serveur
      -
      Publications dans le feed
      (24 dernières heures)
      -
      Items du scénario
      (programmés)
      -
      État de l'exercice
      (en attente)
      Le système fonctionne normalement.
      Informations système
      Version
      1.0.0
      Dernier redémarrage
      -
      Environnement
      -
      Uptime
      -

      Tests API

      Testez la connectivité et les réponses des APIs externes utilisées par PULSE.

      1 Effectuer un test
      Twister API v2
      Recherche de tweets récents
      Maximum 100 résultats

      AI
      Anthropic Claude API
      Génération de texte
      2 Réponse API
      // La réponse brute apparaîtra ici après un test.

      Consommation IA

      Suivi de l'usage Anthropic, des tokens consommés et des économies liées au prompt caching.

      Tokens consommés
      -
      Input - · Output -
      Coût estimé
      -
      Cumulé depuis le démarrage
      Cache hit rate
      -
      En attente de mesures
      Économies cache
      -
      vs coût équivalent sans cache
      Prompt caching Anthropic
      Analyse de l'efficacité du cache sur les prompts système, contextes d'exercice et injects récurrents.
      Cache writes
      -
      Tokens écrits dans le cache
      Cache reads
      -
      Tokens relus depuis le cache
      Efficacité cache
      -
      Reuse moyen par token écrit
      Le prompt caching est configuré sur le contexte d'exercice et les prompts système. Hit rate visible dès la deuxième génération.
      Répartition de la consommation
      Aucun appel enregistré pour l'instant.
      Scénario en cours
      Exercice actuellement actif. Les compteurs persistent au-delà des resets fonctionnels.
      Aucun exercice configuré.
      Modèle dominant -
      Appels Anthropic cumulés 0
      Erreurs 0
      Mesures depuis -
      Activité récente
      50 derniers appels Anthropic, du plus récent au plus ancien.
      Aucun appel enregistré pour l'instant.

      Accès & Sécurité

      Gérez les accès à l'application et renforcez la sécurité de votre environnement.

      Gestion des accès assurée par ORBIT

      ORBIT est l'autorité d'identité unique de la suite SECALYS. La gestion des comptes nominatifs, des passkeys, des sessions et des accès par outil se fait désormais dans l'administration ORBIT. PULSE consomme ces accès via le SSO (connexion « Se connecter avec ORBIT »).

      Les liens d'accès d'exercice (participation par lien) restent gérés ici, ci-dessous.

      Liens d'accès exercice
      Générez un lien JWT signé par rôle : un même lien est partagé à tous les participants du rôle (il ne porte aucune identité individuelle). Action gardée par votre session ORBIT.
      Aucun lien généré pour l'instant.

      Clés API

      Gérez les clés d'accès aux services externes utilisés par PULSE.

      Twister API v2
      Accès à l'API X (Twister) pour la recherche de tweets.
      Non configuré
      Dernière mise à jour : -
      Votre nouvelle clé remplacera l'ancienne après enregistrement.
      AI
      Anthropic Claude API
      Accès à l'API Claude pour la génération de contenu.
      Non configuré
      Dernière mise à jour : -
      Votre nouvelle clé remplacera l'ancienne après enregistrement.
      Bon à savoir

      Les clés API sont stockées de manière sécurisée et chiffrée. Elles ne sont jamais partagées avec des tiers.

      Sources prédéfinies

      Gérez la liste des comptes X (Twister) et des flux RSS de médias web utilisés dans les simulations.

      Sources existantes 0
      Comptes Twister disponibles dans les exercices.
      0 sources au total
      Ajouter un compte Twister
      Ajoutez un nouveau compte à utiliser dans les exercices.
      Suggestions populaires
      Ajoutez rapidement des comptes couramment utilisés.
      Sources existantes 0
      Flux RSS de médias disponibles dans les exercices.
      0 sources au total
      Ajouter un flux RSS
      Ajoutez un nouveau flux RSS à utiliser dans les exercices.
      Suggestions populaires
      Ajoutez rapidement des flux couramment utilisés.

      Journaux

      Activité utilisateur et erreurs enregistrées par le serveur.

      Conservé entre les redémarrages (500 entrées max). Les erreurs serveur sont effacées au redémarrage du serveur.
      Aucune activité enregistrée.
      Aucune erreur enregistrée.
      PULSE
      PULSE
      Platform for Urgent Live Scenario Exercises

      PULSE est un outil pédagogique de simulation de crise médiatique édité par SECALYS. Il permet aux formateurs de reconstituer une crise réaliste en temps réel - publications Twister, articles de presse fictifs, réactions spontanées - pour entraîner les équipes à la communication de crise.

      📡
      Mur de crise en direct
      Deux colonnes synchronisées : réseau social et presse
      ✨
      Injects générés par IA
      Claude Sonnet génère des publications contextualisées
      ⏱
      Scénario programmé
      Publications automatiques avec chronomètre T+
      😡
      Réactions contextualisées
      Haters, supporters et empathiques générés par IA
      🔴
      Sources live
      Comptes Twister réels et flux RSS intégrés
      📈
      Statistiques en temps réel
      Propagation des messages simulée dynamiquement
      La plateforme de résilience SECALYS

      PULSE fait partie d'une suite d'outils conçus par SECALYS pour couvrir l'ensemble du cycle de gestion des situations critiques :

      • SHIELD - Gestion des incidents de sécurité
      • PULSE - Exercices et simulations de crise vous êtes ici
      • PILOT - Pilotage de la crise à chaud
      • ALIVE - Plans de continuité d'activité (PCA)
      ©SECALYS - Tous droits réservés Version 2.1.0-rc.69

      Documentation PULSE v2.1.0-rc.69

      Bienvenue dans la simulation

      PULSE met en scène une crise médiatique fictive en temps réel. Vous y entrez comme observateur d'un mur de publications - tweets, articles de presse, réactions du public - qui se déroulent sous vos yeux comme si la crise se passait maintenant. Votre mission : analyser la situation, identifier les messages clés et préparer des décisions de communication.

      ⚠️ Le contenu affiché est de deux natures. Les publications créées par l'animateur (tweets fictifs, articles simulés, réactions IA) sont entièrement inventées à des fins pédagogiques et n'engagent personne. En revanche, lorsque le badge EN DIRECT est visible, certains contenus proviennent de vraies sources (comptes X officiels surveillés, flux RSS de médias) et reflètent l'actualité réelle du moment.

      Anatomie de l'écran

      L'interface du joueur est structurée en deux colonnes synchronisées et un bandeau de pilotage en haut.

      Twister

      Tweets de l'exercice (comptes fictifs créés par l'animateur, tweets de réactions générés par IA) et - si le scénario l'active - tweets réels de comptes officiels surveillés.

      MP Média Presse

      Articles simulés rédigés par l'animateur, et selon le scénario, articles réels issus de flux RSS de médias nationaux ou régionaux.

      Le bandeau de pilotage

      • Chronomètre T+ - temps écoulé depuis le démarrage de l'exercice (ex. T+12:34).
      • Voyant EN DIRECT - indique si le rafraîchissement automatique des sources réelles est actif.
      • Filtres presse (Tous · France · Régionaux · Internationaux) - filtre la colonne presse par origine du média.
      • Tri - bascule entre « Les plus récents » et « Les plus anciens ».
      • Pilule « N nouvelles publications » - apparaît en haut d'une colonne lorsque vous avez scrollé vers le bas et que de nouveaux items sont arrivés. Cliquez-la pour remonter en douceur.
      • Toggle de rafraîchissement - bouton accessible (rôle ARIA switch) pour activer/désactiver la mise à jour live.

      Lire les indicateurs

      🔥 VIRALPublication marquée par l'animateur comme à fort impact : sa diffusion est volontairement accélérée par la simulation.
      EN DIRECTSource réelle (Twister ou RSS) surveillée en temps réel - le contenu est authentique au moment où il apparaît.
      ♥ likes · 🔁 reposts · 👁 vuesStatistiques de propagation simulées qui croissent dynamiquement pendant l'exercice. Plus une publication est virale ou polarisante, plus la croissance est rapide.
      CitationTweet qui répond ou cite un autre tweet : trace de la dynamique sociale (chaîne d'arguments, mèmes, attaques en miroir).
      T+12minHorodatage relatif au démarrage de l'exercice, affiché sur les publications postées depuis le début de la simulation.

      Les chiffres flashent quand ils sautent significativement, et un petit +N s'affiche brièvement à côté du compteur. Sur un tweet haineux qui décolle, un pulse rouge accompagne la montée.

      Reconnaître les types d'acteurs

      Pendant l'exercice, vous croiserez plusieurs profils - chacun avec son ton et son intention. Apprendre à les distinguer rapidement est l'un des bénéfices pédagogiques de PULSE.

      Compte officielInstitution, entreprise, autorité - souvent ✓ certifié, ton mesuré, communication encadrée.
      JournalisteReprend l'info, la commente, sollicite des réactions. Vérifiez le titre du média et la signature.
      CitoyenTémoin, riverain, anonyme. Apporte du concret mais aussi des rumeurs.
      SupporterApprouve, défend, exprime de la confiance ou de la solidarité.
      EmpathiqueInquiétude, compassion, espoir. Centré sur les victimes ou les conséquences humaines.
      HaterColère, accusations, théories du complot. Cible souvent l'entité mise en cause - parfois l'auteur d'un tweet qui la défend.

      Cliquer, zoomer, ouvrir

      • Cliquer sur un article presse ouvre une vue agrandie (image, titre, chapô, signature) en plein écran. Cliquez ailleurs ou sur la croix pour fermer.
      • Cliquer sur l'image d'un tweet l'ouvre en grand.
      • Cliquer sur le lien externe d'un tweet (si présent) ouvre l'URL d'origine dans un nouvel onglet.
      • Les citations imbriquées dans un tweet sont elles-mêmes lisibles intégralement sans clic supplémentaire.

      Votre mission pendant l'exercice

      • Observer en continu : qui parle ? que dit-on ? à quelle vitesse cela monte ?
      • Distinguer les sources officielles, les journalistes, les rumeurs et les réactions émotionnelles.
      • Identifier les narratifs émergents (soutien, mise en cause, complotisme, ironie…).
      • Repérer le basculement - moment où une rumeur devient dominante, ou un tweet anodin part en viral.
      • Préparer des décisions de communication adaptées : réponse, silence, clarification, prise de parole publique, ton à adopter.

      Ce que vous ne pouvez pas faire

      Le mur joueur est en lecture seule. Vous ne pouvez ni publier, ni effacer, ni modifier les contenus, ni interagir directement avec les comptes affichés. Les outils de réponse sont propres aux animateurs et observateurs. Vos décisions se prennent autour de l'écran (cellule de crise, oral, écrit hors plateforme), pas sur l'écran.

      Conseils pour une session efficace

      • Prenez des notes séparément - sur papier, dans un éditeur ou dans la grille fournie par votre formateur.
      • Marquez l'heure réelle régulièrement pour ne pas perdre le sens du temps simulé.
      • Méfiez-vous des biais : effet de récence (le dernier tweet n'est pas toujours le plus important), confirmation (vous voyez ce que vous cherchez), polarisation (les haters attirent l'œil mais représentent peu).
      • Distinguez fait, opinion, rumeur avant de citer une source dans une décision.
      • Si vous travaillez en équipe, répartissez la veille : un sur X, un sur la presse, un sur les indicateurs.

      FAQ

      Pourquoi un tweet a-t-il « disparu » ?
      Aucun contenu n'est supprimé pendant l'exercice. Le défilement vous l'a sans doute fait passer hors écran : remontez la colonne, ou utilisez le tri « Les plus anciens » pour retrouver l'ordre chronologique inverse.
      Pourquoi je vois la même chose que mon voisin ?
      C'est voulu. Tous les joueurs partagent le même mur, alimenté par le même flux serveur (SSE - Server-Sent Events). C'est l'équivalent d'une « vraie » timeline publique : tout le monde voit la même crise, mais chacun en tire son analyse.
      Y a-t-il du son ou des notifications ?
      Non, l'application n'émet pas de son et n'envoie pas de notification système. Surveillez la pilule « N nouvelles publications » en haut de chaque colonne pour repérer un afflux.
      Et la navigation au clavier ?
      Les contrôles principaux sont accessibles à Tab et activables avec Entrée ou Espace. Le toggle EN DIRECT et les filtres presse exposent les rôles ARIA appropriés (switch, tablist).
      Le contraste me semble faible.
      Le thème PULSE est conçu en lumière directe (texte foncé sur fond clair, ratio supérieur à 12:1 pour le texte principal). Si vous travaillez sur un projecteur peu lumineux, augmentez la luminosité de la salle ou rapprochez le projecteur. Un mode sombre n'est pas disponible dans cette version.
      Puis-je relire l'exercice après coup ?
      Oui - tant que l'animateur n'a pas vidé le mur, l'historique reste consultable. Au-delà, demandez à l'animateur (ou à l'observateur) le journal de l'exercice : les notes prises pendant la session sont la trace la plus fiable.

      Glossaire express

      Tweet
      Publication courte (≤ 280 caractères) sur Twister.
      Repost
      Republication telle quelle d'un tweet existant pour le diffuser à son audience.
      Citation
      Republication accompagnée d'un commentaire - souvent pour soutenir, contredire ou ironiser.
      Viral
      Publication dont la diffusion explose en peu de temps. Marquée 🔥 dans PULSE pour signaler l'effet.
      Narratif
      Histoire dominante qui se forme autour d'un événement (« c'est un complot », « ils ont caché », « solidarité »).
      EN DIRECT
      Indique une source réelle (compte X surveillé, flux RSS d'un média).
      T+
      Temps écoulé depuis le démarrage de l'exercice. T+05:30 = 5 minutes 30 après le top départ.
      Hater
      Compte agressif, accusateur, parfois complotiste. Représente la part toxique d'un débat public.
      Empathie
      Réaction centrée sur les victimes : compassion, inquiétude, espoir.
      Complotisme
      Narratif affirmant qu'un acteur cache la vérité ou orchestre l'événement.
      Nouveau v2.0.0 – mode multi-exercices. Après connexion, vous atterrissez sur « Gérer vos exercices » (liste, KPI, filtres, recherche). Le wizard de création/édition est persistant (nom propre de l'exercice distinct du client, sélecteurs date/heure natifs). Une corbeille réversible remplace la suppression sèche : Supprimer met à la corbeille (récupérable via le bouton Corbeille → Restaurer) ; Purger est définitif (confirmé). Le bouton « Réinitialiser » de la salle de rédaction vide le contenu du run mais conserve l'exercice configuré (contexte + métadonnées). Les espaces de travail (rédaction / simulation / observation) s'ouvrent dans un nouvel onglet. Le champ type d'exercice propose quatre typologies : Crise, Incident Majeur, PCA / PRA, PCI / PRI. Le bouton Dupliquer (sur chaque ligne de la liste et dans le menu « … » de la fiche) crée une copie de l'exercice – sa configuration (contexte, scénario, paramètres) est reprise, mais le déroulé repart de zéro (exercice « (copie) » non démarré).

      Gestes de parc. Cochez plusieurs exercices (cases de ligne, ou la case d'en-tête « tout sélectionner » qui porte sur tout le résultat filtré, pages comprises) : une barre d'actions groupées apparaît au-dessus de la liste — Archiver / Désarchiver / Supprimer dans la liste, Restaurer / Purger dans la corbeille. Seule la purge groupée demande confirmation (irréversible). Les en-têtes de colonnes sont triables : un clic trie, un deuxième clic inverse le sens (indicateur ↑ / ↓).

      Démarrage rapide (5 minutes)

      1. Depuis l'accueil, cliquez Salle de rédaction (mot de passe ou lien d'accès animateur requis).
      2. Renseignez le Contexte de l'exercice (client, scénario, acteurs clés) - ce contexte alimente toute l'IA derrière les injects et les réactions.
      3. Composez quelques publications manuellement ou cliquez ✨ Générer des injects IA.
      4. Programmez les publications avec un décalage T+ minutes et activez les sources live pertinentes.
      5. Cliquez ▶ Démarrer : le chronomètre démarre, les injects se déclenchent à l'heure et les sources live s'allument.
      💡 Pour gagner encore plus de temps, importez un chronogramme Excel (.xlsx) : PULSE en extrait les prompts animateur et génère automatiquement une couverture médiatique IA cohérente avec le contexte. Pour les gros chronogrammes (au-delà de 80 lignes), l'IA est appelée par lots successifs pour que toutes les lignes obtiennent leur couverture média ; le plafond reste 500 lignes par fichier.

      Checklist pré-exercice

      • ☐ Contexte rempli (au minimum : client, objectif, scénario)
      • ☐ 6 à 12 injects scénarisés sur les 30 premières minutes
      • ☐ Une ou deux sources live activées (au moins un compte ou un flux pertinent)
      • ☐ Liens d'accès générés et envoyés aux participants (rôles player / observer)
      • ☐ État des APIs vérifié dans le Configurateur (Twister et Anthropic OK)
      • ☐ Mur de crise testé sur l'écran de projection (si présentiel)
      • ☐ Briefing pédagogique aux participants (mission, durée, débrief)

      Connexion et accès

      Deux moyens d'arriver dans la Salle de rédaction :

      • Mot de passe applicatif - saisi sur la page de connexion. Rôle admin ou player selon le mot de passe utilisé.
      • Lien d'accès signé (JWT) - généré par l'administrateur dans le Configurateur. Le lien encode votre rôle (facilitator, player, observer, admin) et expire après une durée définie (48 h par défaut).

      Configurer le contexte de l'exercice

      Le contexte est la matière première de toute l'IA. Plus il est précis, plus les injects et les réactions sont crédibles.

      Entité Nom de l'entité au cœur de la crise. Ex. Société TranspoRail.
      Objectif pédagogique Ce que les participants doivent travailler. Ex. tenir une posture responsable sous attaque massive.
      Scénario L'événement déclencheur et son contexte factuel. Ex. déraillement TER ; 12 blessés ; cause inconnue ; rumeur de défaillance technique.
      Acteurs clés Liste des PNJ (porte-parole, journalistes, opposants, autorités, victimes) et leur posture initiale.
      ℹ️ Le contexte est inclus dans la génération des réactions haineuses et dans l'import Excel - pour permettre à l'IA de cibler intelligemment l'entité mise en cause vs l'auteur du tweet.

      Composer une publication Twister

      ChampDescriptionLimite
      NomNom affiché du compte (ex. BFMTV, Marie Lefèvre).texte libre
      @handleIdentifiant sans le @.texte libre
      CertifiéAffiche le badge ✓ bleu si oui.Non / Oui
      ContenuTexte du tweet - un compteur affiche les caractères restants.280 caractères
      Image URLURL d'une image illustrative (optionnelle).http(s)
      Likes / Reposts / VuesStatistiques initiales. Croissent ensuite seules pendant l'exercice.nombres
      ViralMarque le tweet comme à fort impact : multiplicateur ×2.8 sur la croissance des stats.Non / Oui 🔥

      Composer un article Média Presse

      ChampDescription
      RubriqueCatégorie éditoriale (Société, Économie, Faits divers, International…).
      TitreLe titre principal de l'article.
      ChapôAmorce d'1 à 2 phrases qui apparaît sous le titre.
      SignatureAuteur et nom du média (ex. Par A. Dupont, Le Quotidien).
      VilleVille de référence, en capitales (ex. NANTES).
      Image URLURL d'une image (optionnel) - l'article est cliquable côté joueur pour zoomer.

      Composer un prompt PNJ (cue animateur)

      Le format Prompt n'apparaît pas sur le mur joueur - c'est une instruction privée pour l'animateur, pour mettre en scène un appel téléphonique, un mail interne, une remontée terrain, etc.

      ChampDescription
      SujetCatégorie courte (Activité, Technique, RH, Communication…).
      RésuméTitre court d'identification.
      ÉmetteurQui appelle / écrit (ex. Cellule de crise SECALYS).
      RécepteurQui reçoit (ex. Teddy Batel - RSI).
      StimuliLe message exact à délivrer aux participants.
      Réaction attendueComportement ou décision qu'on souhaite voir émerger.
      CommentaireNote privée pour l'animateur.

      Génération IA d'injects

      Cliquez ✨ Générer des injects IA. PULSE envoie le contexte de l'exercice au modèle claude-sonnet-4-6 (via l'API Anthropic) et reçoit en retour 5 publications variées : tweets officiels, journalistes, citoyens, articles de presse - équilibrées 50/50 entre X et presse.

      Chaque inject est livré avec une justification pédagogique qui explique son intention. Cliquez Utiliser sur un inject pour pré-remplir le formulaire correspondant - vous pouvez ensuite l'éditer avant de le programmer.

      Conseils pour des injects de qualité

      • Renseignez un contexte concret (lieux, noms, chiffres) - l'IA brille avec des éléments factuels.
      • Précisez la posture du client (transparent, défensif, en retrait, sur la défensive…) dans le scénario.
      • Listez les acteurs clés avec leur ligne (ex. « Mme Roussel, association de victimes - exige des sanctions »).
      • Régénérez (cliquez à nouveau) si le résultat ne colle pas - la production est non-déterministe.

      Limites

      • Quota Anthropic : 20 requêtes/min par défaut (modifiable via RATE_LIMIT_MAX_ANTHROPIC).
      • Les injects sont persistés côté serveur et survivent à un rechargement de page.
      • Un inject « utilisé » n'est pas effacé - il sert de matière jusqu'à ce que vous le supprimiez explicitement.

      « Trouver une image » 🔍

      Le bouton 🔍 Trouver une image, présent sous les champs Image URL des composeurs X et Presse, demande à l'IA de chercher en ligne (via la recherche web Anthropic) une image pertinente pour le contenu rédigé. La requête envoie le contenu du tweet ou le titre/chapô de l'article comme topic. PULSE filtre les résultats qui ressemblent à des logos.

      Si aucune image n'est retournée, c'est généralement que le sujet est trop vague, trop sensible ou que la recherche web n'a pas trouvé de visuel pertinent. Saisissez alors une URL manuellement, ou laissez le champ vide (la publication s'affichera sans illustration).

      Programmation T+ minutes

      Saisissez un décalage T+ (minutes) dans le champ offset-min et cliquez + Programmer. Une fois l'exercice démarré, les publications se déclenchent automatiquement à startTime + offsetMin, côté serveur – indépendamment de l'onglet animateur.

      Dans la liste « Publications programmées » :

      À venirPublication programmée non encore envoyée. Couleur amber.
      EnvoyéePublication déjà publiée sur le mur. Couleur verte.

      Boutons disponibles sur chaque entrée : ▶ Publier maintenant (force l'envoi immédiat), Modifier, Supprimer, et - sur les injects envoyés - les trois boutons de réactions (👍 / 💙 / 😡).

      ℹ️ Publier maintenant hors exercice – l'état « Délivré » n'existe que pendant un exercice lancé. Si vous publiez en immédiat alors que l'exercice n'est pas démarré (ou stoppé), l'inject – quel que soit le canal (Twister, Presse, prompt) – est programmé à T+0 au lieu d'être envoyé : il s'affiche « Programmé » et part automatiquement dès que vous démarrez l'exercice.

      Export / Import JSON et chronogramme Excel

      Le scénario complet (publications + offsets) peut être exporté/importé au format JSON depuis l'interface. Un import Excel (.xlsx chronogramme) est également supporté avec génération IA d'une couverture médiatique cohérente.

      ✅ Planification serveur – un inject programmé à T+10 part à T+10 (à quelques secondes près) même si tous les onglets animateur sont fermés ou en arrière-plan. Un redémarrage du serveur en plein exercice ne perd aucun inject : les injects échus pendant l'arrêt sont rattrapés au redémarrage. Publier maintenant reste un envoi manuel immédiat (hors planification).

      Sources live (Twister + RSS)

      Comptes Twister / X Saisissez un @handle ou choisissez dans les suggestions (@GouvernementFR, @AFP, @BFMTV, etc.). PULSE recherche les tweets récents de ces comptes et injecte les nouveaux dans la colonne X avec le badge EN DIRECT. Polling typique : 60 s côté client.
      Flux RSS Saisissez ou sélectionnez une URL de flux RSS (Le Monde, Figaro, France Info, Ouest France…). Les derniers articles sont récupérés et affichés dans la colonne Média Presse sous le label EN DIRECT. Polling typique : 90 s côté client, avec un cache serveur de 60 s.

      Les sources live sont activées automatiquement au démarrage de l'exercice et coupées au stop. Elles sont limitées à 20 entrées par type.

      Réactions automatiques (👍 💙 😡)

      Sur chaque publication envoyée, trois boutons déclenchent une vague de 6 réactions générées par l'IA. Les tweets n'arrivent pas tous d'un coup : ils sont distribués aléatoirement sur ~1 minute pour un effet de montée réaliste — pendant la distribution, le bouton pulse et son compteur grimpe au fil des arrivées.

      👍 PositifSoutien, confiance, solidarité envers l'entité communicante. Croissance de stats modérée.
      💙 CompatissantEmpathie, inquiétude pour les victimes, espoir, tristesse digne. Ton humain, peu polarisant.
      😡 HaineuxColère, accusations, complotisme. Ciblage contextuel : si le tweet défend l'entité mise en cause, les haters attaquent l'auteur du tweet ; s'il la critique, ils amplifient la critique et attaquent l'entité.

      Dosage pédagogique

      • Une vague positive après une déclaration officielle réussie → renforce la perception de maîtrise.
      • Une vague compatissante après l'évocation des victimes → recentre le débat sur l'humain.
      • Une vague haineuse après un tweet ambigu → met les participants à l'épreuve de l'attaque.
      • Évitez d'enchaîner trois vagues consécutives : laissez le mur respirer entre les pics.

      Indicateurs de réputation

      Le panneau Réputation de la salle de rédaction agrège tonalité (positif / négatif / neutre), volume et tendance, avec un sparkline temporel. La classification est faite par l'IA via /api/sentiment (5 catégories : very_negative, negative, neutral, positive, very_positive). Utilisez-le comme aide à la décision pour ajuster votre prochain inject.

      Gestion de l'exercice

      ▶ DémarrerLance le chronomètre, active les injects programmés et les sources live. État serveur passe à running.
      ⏸ PauseSuspend le décompte : aucun inject ne part tant que l'exercice est en pause (le scheduler serveur ne tique pas un exercice en pause). À la reprise (▶), le chrono et le scheduler repartent du même offset – le temps passé en pause n'est pas compté. La pause survit à la fermeture de l'onglet et à un redémarrage serveur.
      ⏹ StopperArrête l'exercice et vide le mur : tous les injects repassent à l'état « Programmé », le mur joueurs/observateurs est nettoyé. Le scénario et le contexte sont conservés – l'exercice peut être relancé proprement depuis T+0. (Pour un débrief avec mur conservé, faites-le avant de stopper.)
      🗑 ViderEfface toutes les publications du mur (le scénario programmé reste).
      ↺ Reset(Configurateur) Réinitialise l'intégralité de l'état : feed, scénario, sources, contexte, messages observateur. Préserve l'audit (activity log).

      Inviter des participants

      Depuis le Configurateur, page Accès, choisis l'exercice dans la liste déroulante (peuplée des exercices réels), puis génère un lien d'accès signé pour chaque rôle :

      • player - accès au mur de crise (lecture seule).
      • observer - accès à la salle d'observation (timeline, chat, KPIs).
      • facilitator - accès animateur (composer, programmer, gérer l'exercice).
      • admin - tous les droits, y compris Configurateur.
      🔒 Cloisonnement par exercice (v2) : un lien est désormais lié à un exercice précis. Le porteur – quel que soit son rôle, admin compris – ne peut accéder aux données de run (mur, état, messages) que de cet exercice. Un lien généré pour un exercice supprimé ou inexistant est refusé dès la génération. Pour donner accès à un autre exercice : générer un nouveau lien sur cet exercice.

      Durée de validité par défaut : 48 heures (configurable via ACCESS_TOKEN_TTL_HOURS). Un lien peut être révoqué à tout moment depuis la liste des liens actifs (la session du porteur est coupée en temps réel).

      🔗 Un lien par rôle, partagé : le lien ne contient aucune identité individuelle. Tu génères un seul lien player, un seul lien observer, un seul lien facilitator, puis tu diffuses chacun à tous les participants du rôle — inutile de générer un lien par personne. Dans la liste des liens actifs, ils sont regroupés par rôle. Conséquence : révoquer un lien déconnecte tout le rôle — régénères-en un nouveau pour réinviter.

      Bonnes pratiques pédagogiques

      Briefing

      • Annoncer la durée de l'exercice et la posture de l'équipe (cellule de crise, comité de pilotage…).
      • Expliquer la nature fictive du contenu et la présence éventuelle de sources réelles.
      • Définir les livrables : décisions, communiqués, posture face à un journaliste.

      Escalade dramaturgique

      • 0-10 min : faits, premières remontées terrain, articles courts.
      • 10-25 min : montée de la pression - viral, journalistes qui appellent, premiers haters.
      • 25-40 min : pic - rumeurs, théories, réactions politiques, déclencheurs critiques.
      • 40 min+ : retombée orchestrée ou point de bascule sur un sujet adjacent.

      Débriefing

      • Faire identifier le point de bascule par les participants - quand la situation a changé de nature.
      • Reprendre 3 à 5 publications-clés et discuter la décision prise (ou non).
      • Noter ce qui a manqué : info, posture, réactivité, coordination.

      Dépannage animateur

      SymptômeCause probableAction
      « Génération IA » échoue avec un 429Quota Anthropic atteintAttendre 60 s ou augmenter RATE_LIMIT_MAX_ANTHROPIC
      L'IA répond mais avec du contenu hors-sujetContexte vide ou trop vagueRenseigner client / scénario / acteurs précisément, puis régénérer
      « Trouver une image » ne renvoie rienRecherche web sans matchSaisir une URL manuelle ou laisser vide
      Une source Twister ne renvoie rienQuota Twister ou compte vide récemmentVérifier dans Configurateur → Tests → Twister
      Un flux RSS ne renvoie rienURL invalide ou flux indisponibleTester l'URL dans un navigateur ; corriger ou retirer
      Un inject ne se déclenche pasOnglet animateur masqué très longtempsRevenir à l'onglet ; cliquer Publier maintenant si nécessaire
      « 401 Unauthorized » sur une mutationToken expiré ou rôle insuffisantSe reconnecter avec un lien facilitator ou admin

      Raccourcis clavier

      PULSE n'expose pas de raccourcis clavier globaux dans cette version. La navigation se fait au Tab sur les contrôles, et la touche Entrée dans les champs Source Twister et Source RSS ajoute la source.

      Le rôle de l'observateur

      L'observateur suit l'exercice sans pouvoir intervenir sur le mur de crise. Il dispose en revanche d'outils dédiés que les joueurs ne voient pas : timeline détaillée du scénario, indicateurs de fréquentation, bloc-notes personnel et chat avec l'animateur. Sa mission : évaluer, conseiller, capitaliser.

      ⚠️ Restez en retrait pendant l'exercice - pas de spoiler aux joueurs, pas de coup de pouce non concerté avec l'animateur. Vos observations servent au débrief.

      Différences avec la vue joueur

      ÉlémentJoueurObservateur
      Mur de crise (X + Presse)Plein écran, deux colonnesNon - vue centrée sur le scénario
      Timeline du scénario-Liste des injects passés et à venir
      Chat avec l'animateur-Oui (panneau collapsible)
      Bloc-notes-Oui (sauvegardé dans le navigateur)
      KPIs (joueurs connectés, prochain inject…)-Oui
      Statut de l'exerciceImplicite (chronomètre)Badge explicite (REPOS / LIVE / EN PAUSE / TERMINÉ)

      Se connecter en observateur

      L'observateur arrive via un lien d'accès signé de rôle observer, généré par l'animateur ou l'administrateur depuis le Configurateur. Pas de mot de passe à retenir.

      Si vous n'avez reçu qu'un jeton brut, collez-le dans l'onglet Lien d'accès de la page de connexion : PULSE le redirigera automatiquement vers la salle d'observation.

      Timeline du scénario

      La salle d'observation affiche les injects programmés en deux sections :

      ✓ Injects passésPublications déjà envoyées sur le mur, ordre du plus récent au plus ancien.
      À venirInjects programmés, avec un countdown live Dans HH:MM:SS. Si l'exercice est en pause, l'affichage devient Dans N min.

      Cliquez un inject passé (X ou presse) pour ouvrir sa fiche détaillée. Les prompts animateur ouvrent une modale dédiée avec les champs Émetteur / Récepteur / Stimuli / Réaction attendue.

      Chat observateur

      Panneau collapsible relié à la route /api/observer-messages. Permet d'échanger avec l'animateur sans passer par les joueurs : poser une question, signaler un dysfonctionnement, proposer un inject.

      • Un badge unread compte les messages reçus quand le panneau est replié.
      • Les bulles sont alignées à gauche (animateur) ou à droite (observateur) selon l'auteur.
      • L'historique est conservé tant que l'exercice n'est pas reset (max 200 messages).

      Bloc-notes personnel

      Notes locales, sauvegardées dans le localStorage du navigateur (clé pulse_observer_notes_v2). Maximum 100 entrées horodatées.

      • + Ajouter : crée une nouvelle note datée (heure courante).
      • × : supprime une note.
      • 🗑 Effacer tout : vide toutes les notes.
      • 📋 Copier : copie l'ensemble des notes au format [HH:MM] texte, prêt à coller dans un rapport.
      ⚠️ Les notes sont privées et locales - elles ne quittent pas votre navigateur. Si vous changez de poste ou videz votre cache, elles sont perdues. Copiez-les avant la fin de la session.

      KPIs en temps réel

      IndicateurSens
      Salles connectéesNombre total de connexions actives (toutes pages confondues).
      JoueursNombre de connexions de rôle player.
      RestantsInjects programmés non encore envoyés.
      Prochain injectNom + horodatage T+ du prochain inject programmé. ✓ Tous envoyés si le scénario est terminé.

      Construire un rapport d'observation

      PULSE n'inclut pas d'export automatique des observations. Voici la méthode recommandée :

      1. Pendant l'exercice : prenez vos notes au fil de l'eau dans le bloc-notes (un événement clé = une note).
      2. À la fin : 📋 Copier le bloc-notes dans votre traitement de texte.
      3. Capturez les KPIs finaux et le statut de l'exercice (REPOS / TERMINÉ).
      4. Notez 3 à 5 moments charnières repérés (basculement, raté, bonne décision, montée virale).
      5. Ajoutez votre évaluation de l'équipe : posture, coordination, vitesse de réaction, qualité des décisions.

      Bonnes pratiques d'observation

      • Repérer les biais d'équipe : effet de tunnel sur un seul narratif, sous-estimation d'un acteur, sur-réaction à un viral isolé.
      • Mesurer la latence entre l'arrivée d'un inject critique et la première décision.
      • Distinguer signal et bruit dans les haters : un viral haineux peut être un faux drapeau du scénario.
      • Ne pas spoiler via le chat : posez vos questions à l'animateur, pas aux joueurs.
      • Préparez à l'avance une grille d'évaluation (critères : réactivité, qualité des arbitrages, communication interne, cohérence externe).

      Limites connues

      • Aucune intervention sur le mur n'est possible depuis la vue observateur.
      • Le bloc-notes est strictement local - non sauvegardé côté serveur, non partageable depuis l'app.
      • Les messages observateur sont plafonnés à 200 par exercice.
      • Pas d'export PDF automatique : recopiez à la fin.
      Sommaire
      1. Vue d'ensemble
      2. Architecture de déploiement
      3. Prérequis
      4. Variables d'environnement
      5. Installation locale
      6. Déploiement en production
      7. Configurateur (UI admin)
      8. IAM nominatif (Accès & Sécurité)
      9. Liens d'accès signés
      10. Logs et observabilité
      11. Sécurité
      12. Sauvegarde et restauration
      13. Mise à jour
      14. Runbook incidents
      15. Monitoring et SLO
      16. RGPD et confidentialité
      17. Procédure de purge
      18. FAQ admin

      1. Vue d'ensemble

      PULSE est une application Node.js / Express servie en SPA. Le backend expose une API REST (et un canal SSE) consommée par un frontend HTML/CSS/JS vanilla. Le stockage est dual : fichier data/store.json pour le développement local, Azure Cosmos DB en production. Les intégrations externes sont l'API Anthropic (Claude Sonnet 4) et l'API Twister v2, plus la lecture de flux RSS.

      Cette doc couvre la version 2.1.0-rc.69. Vérifiez CHANGELOG.md pour les évolutions.

      2. Architecture de déploiement

      ┌──────────────┐    HTTPS    ┌─────────────────────────────────┐
      │  Navigateurs │────────────▶│         Express (Node 18+)      │
      │  (joueurs,   │◀────────────│  ┌───────────────────────────┐  │
      │   anim, obs) │  SSE / JSON │  │  Routes /api/*  + /auth   │  │
      └──────────────┘             │  │  helmet · CORS · rate-lim │  │
                                   │  └───────────────────────────┘  │
                                   │  ┌───────────────────────────┐  │
                                   │  │  memoryStore (in-process) │  │
                                   │  └─────┬───────────────┬─────┘  │
                                   └────────┼───────────────┼────────┘
                                            │               │
                                    ┌───────▼─────┐  ┌──────▼──────┐
                                    │ data/store  │  │  Cosmos DB  │
                                    │   .json     │  │  (optional) │
                                    └─────────────┘  └─────────────┘
      
                 Externes :
                 ┌────────────┐   ┌────────────┐   ┌────────────┐
                 │ Anthropic  │   │  Twister   │   │  RSS feeds │
                 │ Claude API │   │  v2 API    │   │  (HTTP)    │
                 └────────────┘   └────────────┘   └────────────┘
      • Port d'écoute : variable PORT (défaut 3000).
      • Processus : un seul process Node (pas de cluster). Préférer un reverse-proxy (Nginx, Caddy, Azure App Service) pour HTTPS et la terminaison TLS.
      • Trust proxy : si derrière un reverse proxy, exporter TRUST_PROXY=1 ou un range CIDR - sinon express-rate-limit et le logging d'IP seront erronés.
      • Stockage authoritative : Cosmos DB si COSMOS_ENDPOINT et COSMOS_KEY sont définis ; sinon fichier local.

      3. Prérequis

      • Node.js ≥ 18 (déclaré dans package.json · engines).
      • npm (livré avec Node).
      • Compte Anthropic avec une clé API valide (modèle claude-sonnet-4-6).
      • Compte Twister Developer avec un Bearer Token v2.
      • Stockage : un disque inscriptible pour data/store.json, ou un compte Azure Cosmos DB (recommandé en production).
      • Optionnel : Application Insights (Azure) pour la télémétrie.

      4. Variables d'environnement

      Toutes lues dans src/config/env.js. Mettez-les dans un fichier .env à la racine ou dans la configuration de votre PaaS.

      Obligatoires

      VariableDéfautDescription
      TWISTER_BEARER_TOKEN-Bearer Token Twister (recherche de tweets).
      ANTHROPIC_API_KEY-Clé API Anthropic (génération injects, réactions, sentiment).

      Réseau et runtime

      VariableDéfautDescription
      PORT3000Port d'écoute Express.
      NODE_ENVdevelopmentObligatoire en prod (active cookies secure, log info, désactive les stack traces). À vérifier post-déploiement via GET /api/status → champ system.env.
      ALLOWED_ORIGINShttp://localhost:3000Liste séparée par virgules pour CORS. * est refusé au boot en prod. Chaque entrée doit être une URL parseable.
      TRUST_PROXY1 auto sur AzureAuto-détecté sur Azure App Service via WEBSITE_INSTANCE_ID (défaut 1). Sur tout autre reverse proxy (Nginx, Caddy…), poser TRUST_PROXY=1 ou un range CIDR explicitement. JAMAIS true en prod (refusé au boot – rendrait X-Forwarded-For spoofable).

      Authentification – IAM passwordless (WebAuthn) depuis v2.1.0-rc.15

      VariableDéfautDescription
      ACCESS_TOKEN_SECRET-Secret HMAC pour signer les JWT d'exercice (liens /access?token=…). Obligatoire en prod (boot refusé sinon depuis v1.8.6).
      ACCESS_TOKEN_TTL_HOURS4Durée de vie des liens JWT d'exercice (les sessions plus longues passent ttlHours explicite à POST /admin/access-link).
      IAM_WEBAUTHN_RP_IDsecalys.frRelying Party ID WebAuthn (D13). Domaine de cookie au sens passkey : une passkey enrôlée sous secalys.fr fonctionne pour tout sous-domaine (pulse.secalys.fr, pulse-staging.secalys.fr). À ne jamais changer une fois des comptes enrôlés (invaliderait toutes les passkeys).
      IAM_WEBAUTHN_ORIGINSauto (RP-ID + sous-domaines + localhost hors prod)Liste séparée par virgules d'origines attendues pour la validation WebAuthn (ex. https://pulse.secalys.fr,https://pulse-staging.secalys.fr). Surchargeable explicitement.
      IAM_MIN_CREDENTIALS2Nombre minimum de passkeys par compte (anti-lockout R1 / D12a). Refuse la suppression de la dernière passkey en deçà du seuil. Sysadmin = idem (filet en plus des codes de récupération).
      IAM_RECOVERY_CODE_COUNT10Nombre de codes de récupération générés à chaque appel de POST /iam/recovery/codes. Codes à usage unique, hashés SHA-256 au repos, affichés une seule fois.
      IAM_ASSISTED_GRANT_TTL_MIN30Durée (minutes) du jeton d'enrôlement (enrolToken) émis par le sysadmin pour un reset assisté (POST /iam/admin/users/:id/reset-credentials) ou par POST /iam/bootstrap / création de compte. Borné [1 ; 1440].
      IAM_BREAK_GLASS_SECRET-Active l'endpoint de déblocage d'urgence POST /iam/break-glass (lockout total sysadmin). Absent → endpoint 404 (désactivé, posture par défaut). À ne poser que le temps d'une intervention, puis retirer. Secret fort, comparé en timing-safe via le header X-Break-Glass-Secret.
      BOOTSTRAP_SYSADMIN_EMAIL-Email du 1ᵉ sysadmin créé au démarrage (poule/œuf). Voie recommandée en production : crée un compte sysadmin vide (sans passkey, sans mot de passe – la Phase 1 n'existe plus) ; le 1er enrôlement passe par POST /iam/bootstrap gardé par l'invariant « 0 sysadmin » qui retourne un enrolToken.
      PULSE_BASE_URL-URL absolue utilisée pour construire le lien d'accès (ex. https://pulse.exemple.fr).
      TRAINER_TOKEN-Token optionnel à passer dans X-Trainer-Token pour autoriser les mutations /api sans rôle facilitator/admin (cas usage : tests automatisés).
      💡 BOOTSTRAP_SYSADMIN_PASSWORD et IAM_MFA_KEY ont été retirées en v2.1.0-rc.15 (WU11) avec l'auth Phase 1 (mot de passe + TOTP). Si elles traînent dans les App Settings, elles sont ignorées – à supprimer pour la propreté.

      Rate limiting

      VariableDéfautDescription
      RATE_LIMIT_WINDOW_MS60000Fenêtre de comptage en ms.
      RATE_LIMIT_MAX_TWISTER10Requêtes Twister / fenêtre.
      RATE_LIMIT_MAX_ANTHROPIC20Requêtes Anthropic / fenêtre.

      Stockage Cosmos DB (optionnel)

      VariableDéfautDescription
      COSMOS_ENDPOINT-URL du compte Cosmos. Active le stockage Cosmos.
      COSMOS_KEY-Clé primaire Cosmos.
      COSMOS_DATABASEpulseNom de la base.
      COSMOS_CONTAINERstoreNom du container (partition key /type).

      Observabilité (optionnel)

      VariableDéfautDescription
      APPLICATIONINSIGHTS_CONNECTION_STRING-Si présent, initialise Application Insights (auto-collect requêtes, exceptions, dépendances, console).
      AI_SAMPLING_PERCENT100Pourcentage de télémétrie envoyé à Application Insights (1-100). Baisser sans redéploiement si la facture grimpe pendant un incident (boucle d'erreur, retry storm).

      5. Installation locale

      git clone <repo>
      cd simulateur-crise
      cp .env.example .env       # à créer si absent
      # éditer .env avec vos clés API
      npm install
      npm run dev                # nodemon, hot reload
      # ou
      npm start                  # mode production simple
      npm test                   # jest

      Au premier démarrage, data/store.json est créé s'il n'existe pas. L'application est disponible sur http://localhost:3000.

      6. Déploiement en production

      Pipeline officiel – push main → PROD/staging auto, slot swap manuel pour le live

      Le dépôt déploie via un seul workflow GitHub Actions, sur le slot staging de l'App Service pulse-simulator-prod. Le slot production (live, https://pulse.secalys.fr) ne se met à jour qu'après un slot swap manuel.

      ÉtapeDéclencheurCible
      Déploiement PROD/stagingPush sur main (auto)pulse-simulator-prod slot staging
      Mise en liveSlot swap manuelpulse-simulator-prod slot production

      Flux nominal :

      1. PR mergée sur main → le workflow PROD/staging part automatiquement, version disponible sur le slot staging.
      2. Validation fonctionnelle sur le slot staging (Cosmos PROD données réelles).
      3. Slot swap dans Azure Portal → App Service → Deployment Slots → Swap (source: staging, target: production). Ou en CLI : az webapp deployment slot swap --resource-group <rg> --name pulse-simulator-prod --slot staging --target-slot production.
      4. Swap instantané et réversible – re-swap dans l'autre sens pour rollback.
      💡 L'App Service pulse-simulator-dev a été supprimée le 2026-05-09 au profit de cette architecture single-app à deux slots. Le slot staging tourne sur la même Cosmos que prod, ce qui donne une validation plus fidèle qu'un environnement DEV séparé.

      Azure App Service (provisionnement initial / fork)

      1. Créer une App Service Linux avec le runtime Node 18 LTS.
      2. Configurer les variables d'environnement dans Configuration → Paramètres applicatifs. Ne jamais committer .env.
      3. Activer HTTPS uniquement et un domaine custom avec certificat managé.
      4. Définir NODE_ENV=production. TRUST_PROXY est auto-détecté sur App Service (défaut 1) – ne le poser que si le setup utilise une chaîne de proxies non-standard.
      5. (Recommandé) Provisionner un compte Cosmos DB et renseigner COSMOS_ENDPOINT / COSMOS_KEY.
      6. (Recommandé) Lier une instance Application Insights - la string de connexion est lue automatiquement.
      7. Déploiement par git push, GitHub Actions, ou ZIP deploy. Build = npm install --omit=dev, start = npm start.

      Conteneur (Docker générique) à confirmer

      Aucun Dockerfile n'est fourni dans le repo. Pour packager :

      FROM node:20-alpine
      WORKDIR /app
      COPY package*.json ./
      RUN npm ci --omit=dev
      COPY . .
      ENV NODE_ENV=production
      EXPOSE 3000
      CMD ["node", "src/server.js"]

      Gestion des secrets

      • Ne jamais commiter de .env contenant des clés réelles.
      • Sur Azure, utiliser Key Vault (référencé depuis App Service Configuration).
      • Documenter le canal de rotation des clés (Anthropic, Twister, JWT, mots de passe applicatifs).

      Durcissement runtime des slots (état appliqué – 2026-05-17)

      Les deux slots (production = slot par défaut, staging = --slot staging) sont configurés avec les paramètres runtime ci-dessous. À reporter à l'identique sur tout nouveau slot ou fork.

      ParamètreValeurPourquoi
      httpsOnlytrue (les 2 slots)Trafic en clair refusé (cookie de session, JWT du lien /access?token=, body POST). Une requête HTTP est redirigée 301 vers HTTPS.
      alwaysOntrueL'app ne s'endort plus après ~20 min d'inactivité – la connexion SSE temps réel n'est plus rompue, plus de cold-start au réveil.
      use32BitWorkerProcessfalse (64-bit)Plafond mémoire et perf JS alignés sur le runtime NODE|22-lts Linux.
      http20EnabledtrueHTTP/2 : multiplexage des flux SSE sur un seul TCP (sinon ~6 connexions max par origine côté navigateur).
      healthCheckPath/api/statusRoute publique, 200 inconditionnel, montée avant le gate d'auth (src/app.js). Permet le redémarrage auto d'une instance malade et sert de contrat au smoke test de déploiement (#140).

      Logs plateforme – filet hors Application Insights (#137), activés sur les deux slots : applicationLogs.fileSystem = Information (le log stream montre la sortie pino / requestId), detailedErrorMessages = true (stack trace sur 5xx), failedRequestsTracing = true, httpLogs.fileSystem = true (cap 35 Mo déjà posé). Filet si Application Insights cesse d'ingérer (incident région, quota, mauvaise config workspace #138).

      Vérifier l'état après tout (re)provisionnement ou passage du wizard OIDC (qui peut réécrire la config du site) :

      RG=rg-pulse-prod ; APP=pulse-simulator-prod
      for SLOT in "" "--slot staging"; do
        az webapp show   -g $RG -n $APP $SLOT --query "{httpsOnly:httpsOnly}" -o jsonc
        az webapp config show -g $RG -n $APP $SLOT --query "{alwaysOn:alwaysOn,use32:use32BitWorkerProcess,http20:http20Enabled,healthCheckPath:healthCheckPath}" -o jsonc
        az webapp log show -g $RG -n $APP $SLOT --query "{appLogFs:applicationLogs.fileSystem.level,detailedErrors:detailedErrorMessages.enabled,failedReqTracing:failedRequestsTracing.enabled,httpLogsFs:httpLogs.fileSystem.enabled}" -o jsonc
      done
      ⚠️ Modifier la configuration runtime recycle le worker du slot concerné (courte interruption SSE) – opérer hors exercice live. httpsOnly sur staging : vérifier d'abord qu'aucun outil interne ne tape en http://.

      Accès SCM / Kudu – Basic Auth désactivé (AAD-only, 2026-05-17)

      L'endpoint d'administration SCM/Kudu (déploiement ZIP, console, log stream, dump d'environnement) n'accepte plus que l'authentification Microsoft Entra (AAD) : le Basic Auth (publishing profile / mot de passe SCM, réutilisable depuis n'importe où) est désactivé sur les deux slots.

      Slotscm basic authftp basic auth
      productionfalsefalse
      stagingfalsefalse

      Le pipeline GitHub Actions déploie en OIDC (azure/login + token AAD, aucun publishing-profile) – il n'est pas impacté (validé par un déploiement de test après le changement). L'accès admin manuel passe par le Portail Azure ou az (AAD), sans secret statique.

      Vérifier l'état (à rejouer après tout (re)provisionnement ou wizard OIDC) :

      RG=rg-pulse-prod ; APP=pulse-simulator-prod
      for SCOPE in "sites/$APP" "sites/$APP/slots/staging"; do
        for POL in scm ftp; do
          az resource show -g $RG --namespace Microsoft.Web \
            --resource-type basicPublishingCredentialsPolicies \
            --name $POL --parent "$SCOPE" --query properties.allow -o tsv
        done
      done
      💡 Restriction réseau SCM (scmIpSecurityRestrictions = Allow all) volontairement non posée : risque résiduel faible une fois le Basic Auth coupé (accès Kudu = token AAD court, scoped, MFA-able ; plus de secret statique réutilisable). Une allowlist IP serait fragile (IP des runners GitHub Actions mouvantes) pour un gain marginal – risque résiduel accepté.

      7. Configurateur (UI admin)

      Réservé au rôle admin. L'icône engrenage de l'en-tête a été retirée : le Configurateur s'ouvre désormais par navigation directe (deep-link #ws=configurator). Pages :

      État

      • Statut des APIs externes (Twister, Anthropic) : OK / KO avec dernier test.
      • Métriques runtime : version, uptime, environnement, taille du feed/scénario, état de l'exercice.

      Tests

      • Test Twister : exécute une requête de recherche brute pour valider le bearer.
      • Test Anthropic : envoie un prompt minimal pour valider la clé et la connectivité.
      • Sortie en clair pour debug.

      Accès & Sécurité

      • Écran IAM nominatif (onglets Vue d'ensemble / Comptes / Accès applicatif / Sessions / Stratégies), piloté par public/js/iam.js, gardé par la session serveur nominative.
        • Vue d'ensemble : 4 KPI (comptes actifs, sessions actives, comptes avec passkey de secours en %, comptes désactivés) ; carte « État de sécurité » avec badge de conformité calculé (Conforme / Partiellement conforme / Non conforme) et 4 contrôles cliquables (passkey de secours, codes de récupération, sysadmins actifs, audit) renvoyant vers l'onglet Comptes ; cartes Actions rapides et Informations clés. La couverture « passkey de secours » mesure les comptes à ≥IAM_MIN_CREDENTIALS passkeys (défense en profondeur anti-lockout, pas un seuil bloquant) ; « codes de récupération » compte les comptes ayant au moins un code non consommé (2ᵉ filet, D12b).
        • Comptes : création ({ email, displayName?, role } → réponse { user, enrolToken, expiresAt } à transmettre hors-bande au titulaire pour son 1ᵉ enrôlement), liste avec compteur passkeys + état codes de récupération, reset assisté sysadmin (révoque passkeys + sessions, ré-émet un enrolToken usage-unique).
        • Sessions : liste + révocation autoritaire (le cookie applicatif iam_sid est lié à la session – révocation = coupe immédiate de tout /api/*, pas seulement de l'écran admin).
        • Stratégies : champs lecture seule (D10) – rpId, minCredentialsPerAccount, webauthnUvRequired, TTL sessions admin/sysadmin, fenêtre + seuil de lockout.
      • Sur son propre compte, le panneau IAM expose : « Ajouter une passkey à cet appareil » (enrôle une 2ᵉ / 3ᵉ clé, indispensable pour rester sous IAM_MIN_CREDENTIALS), « Régénérer mes codes de récupération » (révélés une seule fois, à conserver hors-bande), liste + suppression de ses propres passkeys (garde min. impose ≥2 – refuse la suppression de l'avant-dernière).
      • Mire de connexion (WU9 + v2.1.0-rc.19) : section passkey primaire avec boutons « Se connecter avec la passkey » + « Utiliser une autre passkey » (login usernameless, aucun champ – navigator.credentials.get, allowCredentials vide), entrée « Utiliser un code de récupération » (redeem du code → jeton d'enrôlement immédiat sans session → ré-enrôlement d'une passkey directement depuis la mire), et entrée « Activer mon accès avec un jeton d'enrôlement » (panel tab-activate) — un seul champ pour coller l'enrolToken reçu hors-bande après création de compte ou reset assisté, sans étape recovery/redeem préalable (un compte fraîchement créé n'a pas encore de codes de récupération).
      • Greenfield / poule&œuf : sur une install vierge (BOOTSTRAP_SYSADMIN_EMAIL non posé ou compte sysadmin pas encore enrôlé), POST /iam/bootstrap { email } est ouvert tant qu'aucun sysadmin actif n'existe – il crée le compte vide et retourne un enrolToken pour son 1ᵉ enrôlement de passkey via /iam/webauthn/register/*. Auto-fermé ensuite (409 dès qu'un sysadmin existe).
      • Onglet Accès applicatif : génération de liens JWT d'exercice (exerciseId, role, ttlHours) – gardée par votre session nominative (cookie iam_sid, plus aucune clé partagée). Contrat JWT inchangé (R6) : les liens existants émis avant la bascule passwordless restent valides jusqu'à expiration.
      • Historique en mémoire des liens créés pendant la session.

      Clés API

      • Inputs masqués pour TWISTER_BEARER_TOKEN et ANTHROPIC_API_KEY.
      • Rotation à chaud : la sauvegarde appelle POST /api/settings/keys, qui réécrit .env et met à jour les variables process en mémoire.
      • Audit : événement API_KEY_UPDATED dans le journal d'activité.

      Sources

      • Personnalisation des suggestions (comptes Twister et URLs RSS) proposées aux animateurs.
      • Stocké dans store.suggestedSources.

      Journaux

      • Onglet Activité : 500 dernières actions persistées (login, logout, password change, key update, state change, reset, feed cleared, access link). IPs tronquées.
      • Onglet Erreurs : erreurs serveur en mémoire (effacées au redémarrage).
      • Boutons : ↻ Actualiser, 🗑 Purger l'activité (admin), 🗑 Effacer les erreurs.

      8. IAM nominatif – comptes admin/sysadmin (passwordless WebAuthn)

      Sources : src/security/iamUsers.js, iamSession.js, iamWebauthn.js, iamRecovery.js, src/routes/iamAuth.js, iamAdmin.js. Depuis v2.1.0-rc.15 (WU11), l'auth humaine est exclusivement WebAuthn / passkey : tout mot de passe et TOTP a été retiré, l'écran de connexion ne propose plus que la passkey ou un code de récupération.

      • Comptes : admin (« Administrateur fonctionnel » – opère & lit) et sysadmin (« Administrateur global » – gère aussi les comptes). Email + rôle, aucun mot de passe (champ users.pwd retiré du modèle, jeté au boot par _normalizeUser). D7 : l'UI ne montre les actions mutantes (créer/désactiver/supprimer/reset assisté/tout révoquer) qu'au sysadmin ; un admin est en lecture seule (le serveur refuse 403 de toute façon).
      • Login passwordless WebAuthn (D2′/D3′) : POST /iam/webauthn/login/options|verify – usernameless (passkey découvrable, allowCredentials: [], l'OS/navigateur choisit la clé), User Verification = 2ᵉ facteur (biométrie/PIN), défi usage-unique lié IP (anti-rejeu R4), RP ID = IAM_WEBAUTHN_RP_ID (défaut secalys.fr). Session pleine émise à la vérification (plus de mfaPending).
      • Enrôlement & gestion des passkeys : POST /iam/webauthn/register/options|verify + GET/DELETE /iam/webauthn/credentials – gardés par session pleine ou par enrolToken usage-unique (cf. bootstrap / reset / recovery). Garde minimum : ≥IAM_MIN_CREDENTIALS passkeys par compte (défaut 2, anti-lockout R1 / D12a) – refuse la suppression de l'avant-dernière.
      • Récupération anti-lockout (codes – D12) : POST /iam/recovery/codes (session pleine) génère IAM_RECOVERY_CODE_COUNT codes à usage unique, hashés SHA-256 au repos, affichés une seule fois à conserver hors-bande (gestionnaire de mots de passe, coffre-fort). POST /iam/recovery/redeem (public, authLimiter, anti-énumération R4 – réponse générique en cas d'échec) → jeton d'enrôlement usage-unique pour ré-enrôler une passkey sans session après perte de tous les authentificateurs.
      • Reset assisté sysadmin (D12c) : POST /iam/admin/users/:id/reset-credentials (réservé sysadmin, D7) – pour un compte verrouillé, révoque toutes ses passkeys + sessions actives et émet un enrolToken à usage unique, TTL IAM_ASSISTED_GRANT_TTL_MIN (défaut 30 min), à transmettre hors-bande. Le titulaire ré-enrôle ensuite une passkey via /iam/webauthn/register/* sans session.
      • Sessions serveur persistées (cookie opaque iam_sid, httpOnly, Secure en prod, SameSite=Lax) : TTL 12 h (admin) / 8 h (sysadmin), inactivité 2 h, listables & révocables. Verrouillage compte après 5 échecs / 15 min (à la fenêtre près). Révocation autoritaire : le cookie applicatif est lié à la session – révoquer (ou expiration / inactivité / désactivation) coupe immédiatement tout /api/*, pas seulement l'écran admin.
      • Bootstrap 1ᵉ sysadmin (greenfield) : POST /iam/bootstrap { email }, gardé par l'invariant « 0 sysadmin actif » – auto-fermé (409) dès qu'un sysadmin existe. Réponse { user, enrolToken, expiresAt } : le jeton émis sert à enrôler la 1ʳᵉ passkey via /iam/webauthn/register/*. Variante App Settings : BOOTSTRAP_SYSADMIN_EMAIL au démarrage crée un compte sysadmin vide au boot (idempotent) ; l'enrôlement initial passe alors par le même flow /iam/bootstrap.
      • Break-glass (déblocage d'urgence) : POST /iam/break-glass { email } – mécanisme de dernier recours quand tous les sysadmins sont verrouillés (aucune passkey, aucun code de récupération) et que /iam/bootstrap est fermé. Désactivé par défaut : l'endpoint répond 404 tant que la variable d'environnement IAM_BREAK_GLASS_SECRET n'est pas posée. Une fois le secret configuré, un appel avec le header X-Break-Glass-Secret (comparé en timing-safe) émet un enrolToken pour le compte ciblé, injecté directement dans le store du worker. Le titulaire enrôle ensuite une passkey via l'écran « Activer mon accès ». À n'activer (poser le secret) que le temps de l'intervention, puis retirer le secret – l'endpoint redevient 404. Audité (IAM_BREAK_GLASS / IAM_BREAK_GLASS_DENIED).
      • Invariant « dernier sysadmin » : impossible de désactiver, supprimer ou rétrograder le dernier sysadmin actif (refus serveur 409).
      • Audit nominatif : création/maj de compte, reset assisté, génération/redeem de codes de récupération, enrôlement/suppression de passkey, révocation session, génération/révocation de lien d'exercice → journal d'activité (qui agit sur qui ; jamais de secret en clair, codes/clés jamais loggés).
      • Cloisonnement : un JWT d'exercice (player/observer/facilitator) ne donne jamais accès au Configurateur ; l'IAM passe par la session serveur, pas par le JWT d'exercice.
      💡 En production, posez BOOTSTRAP_SYSADMIN_EMAIL au 1ᵉ déploiement pour pré-créer le compte sysadmin vide. Au 1ᵉ démarrage : ouvrez POST /iam/bootstrap { email } pour récupérer un enrolToken et enrôler la 1ʳᵉ passkey. Enrôlez immédiatement ≥2 passkeys (filet anti-lockout) et générez des codes de récupération conservés hors-bande.
      ⚠ IAM_WEBAUTHN_RP_ID ne doit jamais changer une fois des comptes enrôlés : sa rotation invalide toutes les passkeys (chaque clé est liée au RP-ID au moment de l'enrôlement). Si le domaine change, prévoir une campagne de ré-enrôlement assisté.

      9. Liens d'accès signés (JWT)

      Source : src/security/accessTokens.js, src/routes/admin.js, src/routes/access.js.

      • Algorithme : HS256 (signé avec ACCESS_TOKEN_SECRET).
      • Payload : { exerciseId, role, jti, iat, exp }. Depuis la v2, exerciseId est un vrai id d'exercice du store (plus un texte libre) : POST /admin/access-link refuse (400) un exercice inconnu ou supprimé.
      • Autorisation par exercice 🔒 (v2 / WU5) : le middleware requireExercise exige token.exerciseId === :id sur les routes RUN-DATA scopées /api/exercises/:id/… – sinon 403. Vaut pour tous les rôles, admin inclus. Un participant (player/observer) est aussi confiné en lecture de collection (GET /api/exercises filtré à son exercice, /:id → 404 sur un autre) ; un opérateur (facilitator/admin) garde la vue parc cross-exercice (mutations trainerAuth).
      • Rôles valides : player, observer, facilitator, admin.
      • Durée : ACCESS_TOKEN_TTL_HOURS (défaut 4 h ; ttlHours explicite possible à la génération).
      • Génération : POST /admin/access-link – gardé par la session nominative IAM (admin|sysadmin, MFA validé ; cookie iam_sid). Plus aucun secret partagé (x-admin-key retiré). Le Configurateur choisit l'exercice dans un <select> peuplé de GET /api/exercises.
      • Redemption : GET /access?token=<jwt> - valide, pose un cookie access_token (httpOnly, secure en prod, SameSite=Lax), redirige vers la page du rôle. Un anti-flood (accessRedeemLimiter) ne compte que les sondes à token invalide : les redemptions à token valide sont exemptées, donc une salle entière derrière une même IP n'est jamais bloquée.
      • Révocation : liste de révocation par jti (DELETE /admin/access-links/:id) ; l'événement de révocation est diffusé sur le canal SSE GLOBAL (WU6) et coupe la session du porteur en temps réel, quel que soit son exercice. Changer ACCESS_TOKEN_SECRET invalide d'un coup tous les liens.

      10. Logs et observabilité

      Logger applicatif (pino)

      • Niveau : debug en dev, info en prod.
      • Format : JSON en prod, joliment formaté (pino-pretty) en dev.
      • Source : src/utils/logger.js.

      Application Insights

      Initialisé dans src/app.js si APPLICATIONINSIGHTS_CONNECTION_STRING est défini. Auto-collecte requêtes HTTP, exceptions, dépendances et console. Le sampling est piloté par AI_SAMPLING_PERCENT (défaut 100%).

      💡 Hardening futur – la connexion à Cosmos DB utilise actuellement COSMOS_KEY. La migration vers Azure Managed Identity éliminerait le secret en variable d'environnement et son risque de capture indirecte par les logs ou Application Insights. À planifier quand l'infra sera stabilisée.

      Journal d'activité

      • Source : src/services/activityLog.js.
      • Ring buffer de 500 entrées max, persisté dans le store.
      • Schéma : { ts, type, severity, role, ip, message, meta }.
      • IPs tronquées (/24 en IPv4, deux groupes en IPv6) pour limiter la collecte de données personnelles.
      • Types nominatifs (passwordless) : LOGIN_SUCCESS (passkey vérifiée), LOGIN_FAILED (assertion invalide), LOGOUT, WEBAUTHN_REGISTER, WEBAUTHN_CREDENTIAL_REMOVED, RECOVERY_CODES_GENERATED, RECOVERY_REDEEMED, USER_RESET_CREDENTIALS (sysadmin → user), SESSION_REVOKED. Types applicatifs : API_KEY_UPDATED, ACCESS_LINK_CREATED, STATE_CHANGED, RESET_ALL.
      • Lecture : GET /api/logs (rôles facilitator et admin).
      • Purge : DELETE /api/logs/activity (admin).

      Erreurs serveur

      Buffer en mémoire, effacé au redémarrage (volatile). Lisible via GET /api/logs.

      Consommation IA (Anthropic)

      Source : src/services/anthropicMetrics.js, route GET /api/anthropic-metrics. Onglet Configurateur → Consommation IA.

      • Capture du response.usage de chaque appel Anthropic (input, output, cache_creation, cache_read).
      • Persistence dans store.anthropicMetrics – figures conservées au redémarrage et au /api/reset fonctionnel.
      • Ring buffer des 50 derniers appels (catégorie, modèle, tokens, durée, statut) – base de l'activité récente affichée dans le panneau.
      • Catégories internes : inject_generation, reactions_simulation, media_search, import_context, import_media, sentiment_analysis, tests_api, other. Les appels de l'onglet Tests API sont tracés sous tests_api avec un libellé display dédié « Tests API » – visible dans le panneau mais isolé visuellement (couleur grise) pour distinguer le diagnostic de la prod.
      • Coût estimé : table de pricing par modèle (Sonnet 4, Haiku, Opus 4.7), conversion USD → EUR via EUR_USD_RATE (défaut 0.92). Le coût est agrégé par modèle (chaque modèle avec son propre tarif), ce qui reste juste depuis que les routes triviales tournent sur Haiku (cf. #144) : la tuile Modèle liste tous les modèles réellement utilisés, le plus appelé en tête.
      • Cache hit rate = cache_read / (input + cache_create + cache_read). Cible ≥ 60 % depuis #16. Le panneau affiche le hit rate productif (restreint aux catégories qui posent cache_control : inject_generation, reactions_simulation, sentiment_analysis, import_media) avec le rate global en sous-ligne. Le hit rate global reste bas tant que le caching de media_search (web_search multi-tour, ~95 % du volume) n'est pas activé – voir #131.
      • Économies = différence entre le coût réel et le coût équivalent si tous les tokens cache avaient été facturés au tarif input plein.
      • Export : GET /api/anthropic-metrics/export.csv (50 derniers appels, séparateur point-virgule).
      • Reset : DELETE /api/anthropic-metrics (admin uniquement) – purge cumulés, ring buffer et per-category. Lisible par admin et facilitator.

      11. Sécurité

      Helmet (CSP)

      • scriptSrc : 'self' 'unsafe-inline' (rendu de templates dynamiques).
      • styleSrc : 'self' 'unsafe-inline' https://fonts.googleapis.com.
      • imgSrc : 'self' data: https:.
      • connectSrc : 'self' (le SSE est en same-origin).

      CORS

      Liste blanche depuis ALLOWED_ORIGINS. En production, lister explicitement les domaines (pas de wildcard).

      Rate limiting (express-rate-limit)

      LimiteurCibleLimite par défaut
      globalLimitertout /api/*200 req / 60 s
      authLimiter/iam/bootstrap, /iam/webauthn/login/*, /iam/recovery/redeem8 req / 60 s
      twisterLimiter/api/twister/*10 req / 60 s
      anthropicLimiter/api/anthropic/*, /api/inject*, /api/sentiment20 req / 60 s

      Cookies et sessions

      • Cookie access_token (JWT d'exercice, lien /access?token=…) et iam_sid (session nominative opaque, ouverte par WebAuthn) : httpOnly, Secure en prod, SameSite=Lax.
      • Rôles exercice (player/observer/facilitator) : état dans le JWT. Admin/sysadmin : session serveur persistée & révocable (iam_sid → lookup store ; révocation = coupure immédiate).

      SSRF (route preview)

      /api/preview résout les DNS de l'URL fournie et refuse toute IP privée, loopback ou link-local. Limite de fetch à 150 KB.

      Durcissement

      • NODE_ENV=production obligatoire en prod.
      • HTTPS obligatoire (terminaison au reverse proxy).
      • Restreindre les origines CORS au strict nécessaire.
      • Tourner régulièrement Anthropic, Twister, ACCESS_TOKEN_SECRET (invalide tous les liens JWT d'exercice). ⚠ IAM_WEBAUTHN_RP_ID ne se tourne pas (invaliderait toutes les passkeys enrôlées – campagne de ré-enrôlement assisté requise sinon).

      12. Sauvegarde et restauration

      Mode local (store.json)

      • Fichier unique : data/store.json.
      • Sauvegarde : copie périodique du fichier (cron, sauvegarde du volume).
      • Restauration : remplacer le fichier, redémarrer le process.

      Mode Cosmos DB

      • Document unique de partition type='state', id state.
      • Activer le continuous backup Cosmos pour PITR (point-in-time restore).
      • À la première lecture Cosmos vide, l'app migre store.json → Cosmos automatiquement.

      Reset complet

      POST /api/reset (rôle facilitator ou admin) - vide feed, scénario, sources, contexte, injects, messages observateur. Préserve l'auth et le journal d'activité.

      13. Mise à jour

      1. Lire CHANGELOG.md pour les changements et migrations éventuelles.
      2. Pull du code : git pull.
      3. npm install (réconcilie package-lock.json).
      4. npm test (sanity check).
      5. Redémarrer le service. Au 1ᵉʳ boot d'une version WU3+, Cosmos migre automatiquement l'ancien document unique « state » en multi-documents (1 par exercice) puis le supprime ; opération idempotente.
      6. Vérifier /api/status : version mise à jour, APIs OK.

      14. Runbook incidents

      SymptômeDiagnosticAction
      401 sur /iam/webauthn/login/verifyAssertion WebAuthn invalide (passkey absente/désynchronisée/RP-ID changé), compte verrouillé (5 échecs/15 min) ou rate-limit (8/min)Tester avec une autre passkey enrôlée ; ou redeem d'un code de récupération ; ou reset assisté par un sysadmin ; si tous les sysadmin sont locked → recourir aux codes de récupération hors-bande (cf. runbook bascule WU11)
      403 sur une mutation /apiToken de rôle insuffisantRe-générer un lien facilitator ou admin
      429 sur /api/inject*Rate-limit Anthropic (20/min)Augmenter RATE_LIMIT_MAX_ANTHROPIC ou attendre
      Erreurs Twister récurrentesQuota Twister dev v2 atteint ou bearer invalideTester dans Configurateur, rotater le bearer si besoin
      RSS « EN DIRECT » silencieuxFlux down ou bloquéTester l'URL ; remplacer ; cache 60 s côté serveur
      Mémoire qui gonfleActivity log saturé (peu probable, capé à 500), ou fuite SSEInspecter /api/status, redémarrer si besoin
      Inject programmé non partiScheduler serveur arrêté, ou exercice en paused / sans startTime, ou SSE coupée (l'inject est parti mais le mur ne l'affiche pas)Vérifier l'état via /api/status + le statut de l'exercice ; rétablir la connexion SSE (recharger l'onglet) ; Publier maintenant pour forcer un envoi manuel
      Doublons d'injectsRéimport du même chronogrammeLa dédup serveur (_dedupKey) gère ; sinon vider via DELETE /api/feed
      Store corrompustore.json illisible (JSON invalide)Restaurer la dernière sauvegarde ; ou en mode Cosmos, supprimer le fichier local et redémarrer
      Cosmos indisponibleEndpoint/key invalide ou réseau coupéL'app continue avec le store local en lecture/écriture ; logs warn
      SSE qui se déconnecte sans cesseReverse proxy avec timeout courtAugmenter timeout (> 120 s), désactiver buffering (X-Accel-Buffering: no)
      Redémarrage / slot-swapSIGTERM envoyé par la plateforme (Azure restart, swap, déploiement)Le process draine les connexions SSE puis flush async Cosmos (réécrit le doc shared + tous les exercices) ; en cas d'échec Cosmos, fallback backup synchrone data/store.json. Hard exit à 8 s si tout traîne – un redémarrage propre ne perd pas l'état (cf. issue #465 — fix régression WU3)
      Process mort sans cause visibleException non capturée / rejet de promesse non géréChercher la ligne de log level: fatal (« FATAL – flushing store backup and exiting ») : elle porte le kind et la stack ; un backup store.json a été flushé avant la sortie
      Refus de démarrage [config]Variable d'env numérique invalide (PORT, RATE_LIMIT_*, ACCESS_TOKEN_TTL_HOURS non entier positif)Le boot échoue volontairement (fail-fast) avec [config] <VAR> doit être un entier positif au lieu d'un NaN silencieux – corriger la valeur dans les App Settings puis redémarrer

      15. Monitoring et SLO suggérés

      • Disponibilité de l'endpoint /api/status : SLO 99,5 % par mois.
      • Latence p95 sur /api/feed et /api/state : < 300 ms.
      • Taux d'erreur 5xx : < 0,5 %.
      • Quota Anthropic : surveiller 429 dans App Insights (event Dependency).
      • Connexions SSE : exposées dans le snapshot SSE ; alerte si chute brutale pendant un exercice.

      16. RGPD et confidentialité

      Données collectées

      • Authentification : aucun mot de passe en base (auth exclusivement WebAuthn depuis v2.1.0-rc.15). Pour chaque passkey enrôlée : credentialId, clé publique (jamais privée), signCount, aaguid, transports, label utilisateur. Pour la récupération : codes hashés SHA-256 au repos (jamais en clair après affichage initial). Session opaque iam_sid (cookie).
      • Journal d'activité : timestamp, type, rôle, IP tronquée (/24 en IPv4, 2 groupes en IPv6), message. Pas de nom, pas d'email.
      • Bloc-notes observateur : localStorage du navigateur - jamais envoyé au serveur.
      • Messages observateur : texte libre + horodatage, plafonné à 200 par exercice.
      • Contenu d'exercice (publications, réactions IA, contexte) : pédagogique et fictif. Ne pas y faire figurer de données personnelles réelles.

      Rétention

      • Activity log : 500 entrées (rolling), purge manuelle possible.
      • Erreurs serveur : volatile (effacées au redémarrage).
      • Sessions : durée du JWT (48 h par défaut).
      • Sauvegarde Cosmos : selon politique du compte Cosmos.

      Droit à l'effacement

      • Pas de comptes utilisateurs nominatifs - donc pas de profil à supprimer.
      • Sur demande : purger l'activity log (DELETE), réinitialiser le store (POST /api/reset), supprimer la base Cosmos.

      Sous-traitants externes

      • Anthropic (USA) : reçoit le contexte d'exercice et les contenus saisis pour génération IA. Vérifier les CGU à jour avant tout usage avec données sensibles.
      • Twister : requêtes de recherche en clair (queries publiques).
      • Sources RSS : flux publics.
      • Microsoft Azure (Cosmos, App Insights) : si activés.

      17. Procédure de purge

      1. Vider l'exercice en cours : Configurateur → Tests → Reset (ou POST /api/reset).
      2. Effacer l'activity log : Configurateur → Journaux → 🗑 Purger l'activité.
      3. Effacer les erreurs serveur : redémarrer l'instance OU bouton dédié dans Journaux.
      4. Supprimer le stockage : rm data/store.json (mode local) ou supprimer le document Cosmos.
      5. Faire tomber les sessions : changer ACCESS_TOKEN_SECRET + redéployer (invalide tous les JWT en cours).

      18. FAQ admin

      Comment activer Cosmos DB ?
      Renseigner COSMOS_ENDPOINT et COSMOS_KEY dans l'environnement, puis redémarrer. Au premier démarrage, le contenu du store.json local est migré vers Cosmos (read-once, upsert).
      Passkey perdue / appareil cassé pour un admin ?
      3 paliers selon ce qui est encore disponible.
      ① Le titulaire a une autre passkey enrôlée (cas nominal – IAM_MIN_CREDENTIALS=2 impose ≥2) : il se connecte avec, supprime l'ancienne et ré-enrôle une nouvelle clé.
      ② Plus de passkey mais codes de récupération conservés : depuis la mire, « Utiliser un code de récupération » → POST /iam/recovery/redeem → jeton d'enrôlement immédiat → ré-enrôle une passkey sans session.
      ③ Plus de passkey et plus de codes : un sysadmin lance le reset assisté depuis l'onglet Comptes (POST /iam/admin/users/:id/reset-credentials) → révoque passkeys + sessions et émet un enrolToken usage-unique à transmettre hors-bande.
      Si plus aucun sysadmin n'est accessible : utiliser les codes de récupération du sysadmin (③ self-service) ; en dernier recours greenfield – redéployer avec un nouveau BOOTSTRAP_SYSADMIN_EMAIL ; POST /iam/bootstrap reste ouvert tant qu'aucun sysadmin actif n'existe.
      Peut-on lancer plusieurs instances ?
      Pas en mode local (chaque instance aurait son propre store.json). En mode Cosmos, plusieurs instances peuvent partager le même état - attention toutefois à la cohérence du SSE entre instances (sticky sessions recommandées).
      L'IP n'est pas la bonne dans les logs.
      Définir TRUST_PROXY=1 (ou un range CIDR de votre reverse proxy) pour qu'Express lise X-Forwarded-For.
      Comment révoquer un lien d'accès en urgence ?
      Pas de blacklist dans cette version. Changer ACCESS_TOKEN_SECRET et redéployer pour invalider tous les liens en cours (les sessions ouvertes seront déconnectées au prochain appel /api/me).
      v2.0.0 – mode multi-exercices (BREAKING). Le store est une collection d'exercices (memoryStore : exercises Map id→Exercise + currentExerciseId + façade id-addressable withExercise(id)). Les slices globales (activity, errorLog, metrics, accessLinks, revokedTokens, suggestedSources, l'IAM users/sessions, la slice éphémère challenges (défis WebAuthn + jetons d'enrôlement, usage-unique TTL-bornés) et les conteneurs users.webauthn/users.recovery) restent hors exercice. Un document mono pré-v2 est synthétisé en « exercice #1 » sans migration (Cosmos inchangé). Côté API, rupture franche (décision #7) : les routes par-exercice passent sous /api/exercises/:id/… (middleware requireExercise), plus un routeur collection /api/exercises. Frontend : chokepoint apiUrl() (public/js/api-url.js + allowlist globale) ; SSE wall/observer scopées, exemptées du rate-limiter. WU5 : requireExercise exige token.exerciseId === :id (403 sinon, tous rôles inclus). WU6 : eventBus pub/sub par canal – un onglet abonné à /api/exercises/:id/events ne reçoit que son exercice ; le canal GLOBAL ne porte que la révocation de session.
      v2.1.0-rc.15 – IAM passwordless (BREAKING, dérogation MINOR tracée). L'auth humaine est exclusivement WebAuthn / passkey (WU11). Routes Phase 1 retirées (404) : POST /login, /login/mfa, /auth/logout (légacy SPA) ; POST /iam/login, /iam/mfa/enroll, /iam/mfa/verify, /iam/me/password ; /iam/admin/users/:id/reset-{password,mfa}. Modules retirés : iamMfa.js, appPasswords.js, routes/login.js. Modèle users : champs pwd et mfa retirés (jetés au boot par _normalizeUser). Sessions : mfaPending retiré. Deps : otplib + qrcode retirés (+19 transitifs). BOOTSTRAP_SYSADMIN_PASSWORD et IAM_MFA_KEY ignorées. Traité en MINOR par dérogation tracée (mono-tenant, front mis à jour synchrone) – cf. CHANGELOG v2.1.0 § WU11 et règle CLAUDE.md (non refondue, scope strict). Détails plan : PLAN-IAM-ACCES-SECURITE.md §6.
      Sommaire
      1. Vue d'ensemble du repo
      2. Architecture logique
      3. Modèles de données
      4. API HTTP
      5. Frontend
      6. Scheduler d'injects
      7. Intégration Anthropic
      8. Sécurité côté code
      9. Tests
      10. Conventions
      11. Comment ajouter…
      12. Build et assets
      13. Performance
      14. Comment debugger
      15. Contribuer
      16. Glossaire technique

      1. Vue d'ensemble du repo

      simulateur-crise/
      ├── data/
      │   └── store.json              # État applicatif (mode local)
      ├── public/                     # Assets servis statiquement
      │   ├── index.html              # SPA (toutes les vues) – embarque la mire passkey
      │   ├── css/style.css           # Design system unique
      │   ├── assets/                 # Logos, illustrations
      │   └── js/                     # Modules frontend (vanilla)
      │       ├── bootstrap.js        # Patch fetch (X-Trainer-Token)
      │       ├── auth.js             # Mire passkey + recovery (login/logout)
      │       ├── iam.js              # Écran IAM admin (Comptes / Sessions / …)
      │       ├── app.js              # Orchestration, modes, SSE/polling
      │       ├── wall.js             # Mur joueur
      │       ├── observer.js         # Salle d'observation
      │       ├── newsroom.js         # Salle de rédaction (animateur)
      │       ├── configurator.js     # Configurateur (admin) + modale Doc
      │       ├── context.js          # Édition contexte d'exercice
      │       ├── sources.js          # Gestion sources live
      │       └── reputation.js       # Indicateurs de tonalité
      ├── src/
      │   ├── server.js               # Bootstrap (charge config, init store)
      │   ├── app.js                  # Express : middlewares, montage routes
      │   ├── config/env.js           # Lecture & validation des variables d'env
      │   ├── middleware/             # rateLimiter, trainerAuth, validate, …
      │   ├── security/               # accessTokens (JWT exercices), iamUsers,
      │   │                           # iamSession, iamWebauthn, iamRecovery
      │   ├── routes/                 # voir §4 (iamAuth, iamAdmin, …)
      │   ├── services/               # anthropicService, twisterService, activityLog
      │   ├── repositories/           # storeRepository (Cosmos OU disque)
      │   ├── store/                  # memoryStore + eventBus (pub/sub SSE)
      │   ├── db/cosmos.js            # Client Cosmos + migration store.json
      │   └── utils/                  # logger (pino), diskStore
      ├── src/__tests__/              # Suites Jest + supertest
      ├── package.json                # Scripts: start, dev, test
      ├── README.md
      └── CHANGELOG.md

      2. Architecture logique

      Application en couches strictes :

      ┌────────────────────────────────────────────┐
      │  Routes (src/routes/*.js)                  │  HTTP boundary
      │  - parse body (zod)                        │
      │  - apply middlewares (auth, rate-limit)    │
      │  - 1 fichier = 1 ressource                 │
      └────────────────┬───────────────────────────┘
                       │
      ┌────────────────▼───────────────────────────┐
      │  Services (src/services/*.js)              │  Business logic
      │  - composent plusieurs stores/APIs ext.    │
      │  - sans dépendance Express                 │
      └────────────────┬───────────────────────────┘
                       │
      ┌────────────────▼───────────────────────────┐
      │  Store + Repository                        │  Persistance
      │  memoryStore: source de vérité en mémoire  │
      │  storeRepository: write-through Cosmos +   │
      │                   disque                   │
      └────────────────────────────────────────────┘
      • Pourquoi cette séparation ? Tester sans Express (services purs), pouvoir changer de DB sans toucher aux routes, garder les routes minces (parse + dispatch).
      • État partagé : memoryStore est un objet en mémoire. À chaque mutation, on appelle storeRepository.save() qui persiste vers Cosmos (si activé) puis vers disque en backup.
      • Pub/sub cloisonné (WU6) : eventBus émet à chaque changement de feed/state/messages sur le canal de l'exercice muté (e.id) ; /api/exercises/:id/events souscrit au canal de cet exercice, /api/events (global) au seul canal GLOBAL (révocation de session). Plus aucune fuite SSE inter-exercice.

      3. Modèles de données clés

      Source de vérité du shape : les schémas zod dans src/routes/ (validation à l'entrée). Aperçus :

      Feed item (publication sur le mur)

      {
        "id": "x_1715180000_xy7k",
        "type": "x" | "of" | "prompt",
        "name": "BFMTV", "handle": "BFMTV", "verified": true,
        "content": "...", "img": "https://...",
        "likes": 1240, "reposts": 320, "views": 18400,
        "viral": false,
        "live": false,
        "_serverTime": 1715180000123,
        "tone": "neutral" | "very_negative" | "negative" |
                "positive" | "very_positive",
        "quoted":  { "author": { "name", "username", "verified" }, "content" },
        "replyTo": { "name", "handle", "content" },
        // type 'of' :
        "rubrique": "Société", "headline": "...", "chapo": "...",
        "byline": "Par A. Dupont", "city": "NANTES",
        // type 'prompt' :
        "sujet": "Technique", "resume": "...", "emetteur": "...",
        "recepteur": "...", "stimuli": "...", "reaction": "...",
        "comment": "..."
      }

      Inject programmé (scenario item)

      { "item": { ...feedItem }, "offsetMin": 5 }

      Source live

      // store.sources
      {
        "twister": ["GouvernementFR", "AFP", ...],   // 20 max
        "media":   ["https://www.lemonde.fr/rss/...", ...]  // 20 max
      }

      Activity log entry

      {
        "ts": 1715180000123,
        "type": "LOGIN_SUCCESS",
        "severity": "info" | "warn" | "error",
        "role": "admin" | "player" | "facilitator" | "observer",
        "ip": "203.0.113.0/24",
        "message": "Admin connecté",
        "meta": { /* libre */ }
      }

      User (IAM nominatif, passwordless)

      {
        "id": "usr_…", "email": "alice@…", "displayName": "Alice",
        "role": "admin" | "sysadmin",
        "active": true,
        "createdAt": "…", "createdBy": "usr_…",
        "lockoutUntil": null,
        "webauthn": {
          "userHandle": "uh_…",            // ID stable de l'utilisateur côté WebAuthn
          "credentials": [{
            "credId": "…",                  // base64url
            "publicKey": "…",               // base64url COSE
            "signCount": 42,
            "aaguid": "…",
            "transports": ["internal", "hybrid"],
            "label": "iPhone d'Alice",
            "createdAt": "…", "lastUsedAt": "…"
          }]
        },
        "recovery": {
          "codes": [{ "hash": "sha256:…", "usedAt": null }],
          "generatedAt": "…"
        }
      }
      // pwd / mfa : retirés depuis v2.1.0-rc.15 (WU11). sanitize() les masque toujours.

      Challenge (slice éphémère)

      {
        "id": "ch_…",
        "kind": "login" | "register" | "enrol" | "bootstrap",
        "challenge": "…",       // base64url (32 octets)
        "userId": "usr_…" | null,
        "ip": "203.0.113.0/24",
        "expiresAt": 1715181000000,
        "consumedAt": null      // usage unique R4
      }

      JWT payload

      { "exerciseId": "ex_3f2a…", "role": "player", "jti": "…", "iat": ..., "exp": ... }
      // exerciseId = vrai id store ; requireExercise exige token.exerciseId === :id (403 sinon)

      Contexte d'exercice

      { "client": "...", "objectif": "...", "scenario": "...", "acteurs": "..." }

      4. API HTTP - référence

      Préfixes : /iam (auth passwordless / bootstrap / récupération + /iam/admin/* gestion comptes ; session iam_sid), /access (redemption JWT exercice), /admin (liens exercice, session IAM), /api (reste, globalLimiter). Auth applicative par cookie access_token (JWT exercice) ou Authorization: Bearer. Phase 1 (mdp + TOTP) retirée en v2.1.0-rc.15 (WU11) : les routes /login, /login/mfa, /auth/logout, /iam/login, /iam/mfa/*, /iam/me/password, /iam/admin/users/:id/reset-{password,mfa} répondent 404.

      MéthodeCheminAuthBody / Notes
      POST/iam/bootstrapauthLimiter + invariant 0-sysadmin{ email, displayName? } → { user, enrolToken, expiresAt } ; auto-fermé (409) dès qu'un sysadmin existe
      POST/iam/webauthn/login/options · /verifyauthLimiterLogin passwordless – usernameless (allowCredentials: [], passkey découvrable), UV requis (D3′), défi usage-unique lié IP (R4), session pleine émise à verify
      POST/iam/logoutsessionRévoque la session courante côté serveur + efface iam_sid
      GET/iam/me-Identité nominative (soft, jamais 401) + bootstrap (true tant qu'aucun sysadmin actif)
      POST · GET · DELETE/iam/webauthn/register/options · /register/verify · /credentials[/:credId]session pleine ou enrolToken (self)Enrôlement & gestion des passkeys – residentKey: 'required', attestation none (D14), refuse doublon credId, garde min. IAM_MIN_CREDENTIALS (défaut 2 – refuse la suppression de l'avant-dernière)
      POST/iam/recovery/codessession pleineGénère IAM_RECOVERY_CODE_COUNT codes (défaut 10), hashés SHA-256 au repos, affichés une seule fois
      POST/iam/recovery/redeemauthLimiter (public){ email, code } → { enrolToken, expiresAt } ; réponse générique en cas d'échec (R4 anti-énumération)
      GET · POST · PATCH · DELETE/iam/admin/users* · /iam/admin/sessions* · /iam/admin/security/*session (lecture admin+sysadmin ; mutations sysadmin / D7)CRUD comptes (POST = { email, displayName?, role } → { user, enrolToken, expiresAt }), sessions, summary (KPI webauthn « ≥N passkeys »), policies (rpId / minCredentialsPerAccount / webauthnUvRequired en lecture seule, D10)
      POST/iam/admin/users/:id/reset-credentialssysadmin (D7)Reset assisté (D12c) – révoque passkeys + sessions ; émet un enrolToken usage-unique (TTL IAM_ASSISTED_GRANT_TTL_MIN min, défaut 30) à transmettre hors-bande
      GET/access?token=…-Valide JWT exercice, pose cookie access_token, redirige selon le rôle
      POST/admin/access-linksession IAM (admin|sysadmin){ exerciseId, role, ttlHours? } → { url, token }
      GET/api/statuspublicÉtat APIs + version + uptime
      GET/api/meany{ authenticated, exerciseId, role }
      GET/api/eventsanySSE - { feed, state, connections, messages }
      GET/api/feedany{ items: [...] }
      POST/api/feedtrainerAuthItem validé (zod)
      DELETE/api/feed/:idtrainerAuthSupprime un item
      DELETE/api/feedtrainerAuthVide le mur
      GET / POST/api/scenariotrainerAuthListe / remplacement (max 500)
      GET / POST/api/statetrainerAuth{ status: idle|running|paused|stopped, startTime? }
      POST/api/twister/searchtwisterLimiter{ query, max_results, since_id? }
      GET/api/twister/syndication/:idtwisterLimiterPhotos/videos d'un tweet
      POST/api/anthropic/searchanthropicLimiter{ userPrompt } → { text }
      GET/api/exercises/:id/rssanyFlux presse agrégés des sources media de l'exercice (cache 60 s)
      GET / POST/api/sourcestrainerAuth{ twister[20], media[20] }
      GET / POST/api/contexttrainerAuthContexte d'exercice
      GET / POST / DELETE/api/injecttrainerAuth + anthropicLimiterGénère / lit / vide les injects IA
      POST/api/inject/reactionstrainerAuth + anthropicLimiterVague de réactions (positif / compatissant / haineux)
      POST/api/inject/replenishtrainerAuth + anthropicLimiterRemplace un inject utilisé
      POST/api/inject/find-imagetrainerAuth + anthropicLimiter{ topic } → URL d'image
      POST/api/sentimenttrainerAuth + anthropicLimiterClassifie 5 catégories
      POST/api/previewanySSRF-safe - og:image + og:title
      GET / POST/api/settings/keystrainerAuthLit/met à jour Twister + Anthropic
      GET / POST/api/settings/suggested-sourcestrainerAuthSuggestions UI
      POST/api/resettrainerAuthRéinit complet (préserve audit)
      GET/api/logstrainer (pas player){ errors, activity } (200 max)
      DELETE/api/logs/activityadminVide le journal d'activité
      GET / POST / DELETE/api/observer-messagesanyChat observateur (200 max)
      POST/api/importtrainerAuth (8 MB)Import chronogramme XLSX → IA

      Erreurs standard : 400 (validation zod), 401 (non authentifié), 403 (rôle insuffisant), 429 (rate limit), 500 (erreur serveur). Format : { error: "...", details? }.

      5. Frontend

      Modules et ordre de chargement

      Tous les scripts sont chargés en bas de public/index.html, dans cet ordre :

      1. api-url.js · csrf.js · header-widgets.js – chokepoint API, jeton CSRF, widgets de l'en-tête.
      2. auth.js – mire de connexion passkey + entrée code de récupération (helpers WebAuthn base64url ↔ ArrayBuffer, navigator.credentials.get/create, exports window.WA). Wire wireLogout() sur /iam/logout.
      3. bootstrap.js – patch window.fetch pour injecter X-Trainer-Token depuis localStorage (cas dev / tests).
      4. app.js – état global, connexion SSE, polling fallback, restrictions de rôle, fonctions d'écran (_viewWall, _viewNewsroom…).
      5. router.js – app-shell : routeur hash centralisé (registre de 6 vues, PulseRouter.go(), gardes de rôle, historique pushState + popstate/hashchange). Les show*() historiques y délèguent.
      6. exercices.js, sources.js, context.js, newsroom.js, reputation.js, wall.js, observer.js, configurator.js.
      7. iam.js – écran IAM admin (onglets Vue d'ensemble / Comptes / Sessions / Stratégies), pilote l'enrôlement de passkey et le reset assisté côté UI.

      Conventions

      • Pas de framework - JS vanilla, fonctions globales sur window.
      • État côté client via variables globales préfixées _ pour les internes (_accessRole, _wallSSE).
      • Communication serveur : fetch (avec patch bootstrap) + EventSource pour le SSE.
      • Navigation pilotée par le routeur router.js (popstate / hashchange) ; les autres syncs restent des appels directs aux renderers.
      • Cache-busting via ?v=YYYYMMDDx sur chaque <script> et <link>.

      Polling vs SSE

      /api/events est l'option par défaut. En cas d'échec, app.js bascule en polling toutes les 2 s sur /api/feed + /api/state. L'observateur a un fallback à 3 s (_startObsPoll).

      6. Scheduler d'injects

      Scheduler côté serveur (WU8) – src/services/injectScheduler.js. Le déclenchement n'est plus calculé dans le navigateur animateur (throttling / onglet fermé). Mécanisme :

      • Le serveur stocke, par exercice, scenario: [{ item, offsetMin }], l'état { status, startTime, pausedAt } et firedInjectIds (sent-tracking persisté, hors scénario).
      • Un unique setInterval (tick 3 s) itère tous les exercices running. Pour chaque entrée échue (Date.now() - startTime ≥ offsetMin*60000) non encore déclenchée : insertion via le chemin store withExercise(id).addToFeed (idempotent sur item.id, persist coalescé, broadcast SSE par exercice).
      • Rattrapage au boot : après store.init(), un passage rejoue en rafale les injects échus pendant l'arrêt (idempotent – triple garde firedInjectIds + _feedIds + filtre). Arrêt propre du timer au shutdown gracieux.
      • Pause : un exercice paused n'est pas tické (décompte gelé) ; à la reprise le client rebase startTime (temps en pause non compté) et le scheduler repart au même offset.
      • Cloisonnement : un tick sur l'exercice A ne déclenche jamais les injects de B. Le frontend ne fait plus que reconstruire l'affichage « envoyées » depuis l'appartenance au feed (reçu par SSE).
      • Limite connue : en multi-instance (≥ 2 réplicas, hors périmètre Azure single-slot actuel), chaque instance schedule ; addToFeed dédoublonne le feed mais le sent-tracking est last-write-wins. WU8 améliore strictement le cas « onglet fermé = rien ne part ».

      7. Intégration Anthropic

      • Modèle : sélection par route (#144). Défaut claude-sonnet-4-6 (constantes MODEL_SONNET / MODEL_HAIKU en tête de src/services/anthropicService.js, override via callClaude(..., { model })). Génération d'injects + réactions sur Sonnet 4.6 ; classification de sentiment + distillation de requête visuelle sur Haiku 4.5 (~3× moins cher, tâches déterministes sans raisonnement).
      • Client : SDK officiel @anthropic-ai/sdk (singleton partagé). Le passage en SDK donne pooling de connexions, retry transitoire, mapping d'erreurs typé, et surtout support natif de cache_control.
      • Tool use : recherche web via le tool web_search_20250305 (header beta anthropic-beta: web-search-2025-03-05) pour les injects et la recherche d'images.
      • Boucle d'outils : implémentée dans _callWithWebSearch (max 10 tours, chaque tour passe par client.messages.create du SDK). Depuis #131, un cache_control: ephemeral est posé à chaque tour sur le dernier bloc du dernier message (helper _withCacheBreakpoint) : le tour N+1 relit depuis le cache le préfixe accumulé du tour N (prompt + tool_uses + tool_results précédents) au lieu de le payer plein tarif. C'est le levier qui fait redescendre la part de media_search dans la facture totale (95 % du volume avant #131).
      • Prompts : structurés en system blocks (persona + format spec + contexte d'exercice) pour la portion stable, user message pour la portion variable. Sources inline dans src/routes/inject.js, src/routes/sentiment.js, src/routes/anthropic.js et src/routes/importChronogramme.js.
      • Prompt caching : activé via cache_control: { type: 'ephemeral' } sur (a) les blocs system des routes productives (#16 – /api/inject, /api/inject/replenish, /api/inject/reactions, /api/sentiment, /api/import/chronogramme batches média) et (b) le dernier bloc du dernier message à chaque tour de _callWithWebSearch (#131). /api/inject et /api/inject/replenish partagent volontairement la même entrée de cache → cache réutilisé sur toute la session d'exercice. Anthropic ne cache effectivement qu'au-delà de 1024 tokens cumulés (Sonnet) ; en deçà le breakpoint dégrade silencieusement en no-op. Limite Anthropic : 4 breakpoints max par requête – la stratégie courante en utilise au plus 3 (jusqu'à 2 system blocks + 1 sur le tool-loop).
      • Mesure : tokens et hit rate visibles sur l'onglet Configurateur → Consommation IA (tracé via response.usage.cache_creation_input_tokens / cache_read_input_tokens).
      • Tokens : limites max_tokens par appel - 2500 (injects), 2000 (réactions), 1200 (sentiment), 600 (replenish), 4000 (média import), 2000 (web search).
      💡 Cible attendue : ≥ 60 % de cache hit rate sur une session d'exercice typique (8-10 appels Anthropic en 30 min), -30 à -50 % de latence p95 sur les appels qui réutilisent le contexte. Mesurable en direct depuis le panneau Consommation IA.

      8. Sécurité côté code

      • Validation : zod sur tous les body POST (src/middleware/validate.js wrapper).
      • Auth IAM (passwordless) : requireSession() (session pleine, cookie iam_sid) – cf. src/security/iamSession.js ; vérification WebAuthn déléguée à @simplewebauthn/server (verifyAuthenticationResponse / verifyRegistrationResponse) dans src/security/iamWebauthn.js – D11 / D13 (RP-ID = secalys.fr ou env) / D14 (attestation none) / R8 (signCount toléré à 0 et exigé monotone après).
      • Auth applicative : requireAccess([roles]) et trainerAuth – voir src/security/accessTokens.js et src/middleware/trainerAuth.js.
      • Comparaisons sensibles : crypto.timingSafeEqual sur les codes de récupération hashés (anti-timing-leak) ; signCount monotone strict (anti-clone R8) ; défis & jetons d'enrôlement usage-unique + TTL + liés IP (R4).
      • Anti-énumération (R4) : réponses génériques sur /iam/webauthn/login/verify et /iam/recovery/redeem (aucune fuite « compte inexistant » vs « code invalide »).
      • SSRF : src/routes/preview.js filtre les IPs privées et limite la taille de fetch.
      • Cookies : httpOnly, secure en prod, SameSite=Lax.
      • Helmet : configuration explicite dans src/app.js (CSP avec unsafe-inline à terme à durcir).

      9. Tests

      npm test          # jest, testTimeout 10s, testEnvironment node – 32 suites / 449 tests verts

      Suites présentes dans src/__tests__/ (sélection) :

      • iamWebauthnE2E.test.js – WU12 : E2E avec vraie crypto P-256/ES256, authentificateur factice qui produit des signatures DER vérifiables. Couvre login + register + recovery + reset-credentials sans mocker @simplewebauthn/server.
      • iamWebauthnModelStore.test.js – slice store WebAuthn / recovery (invariants D9 « pas de PII en clair » / D12a garde min credentials).
      • helpers/seedSession.js – sème une session IAM directement dans le store, pour les tests qui n'auditent pas la chaîne d'auth elle-même (exerciseAuthz, tenancyE2e, etc.).
      • accessTokens.test.js – émission/validation JWT d'exercice, expiration, rôles.
      • security.test.js – protections SSRF, validation URLs.
      • feed.test.js, scenarioRoutes.test.js, stateRoutes.test.js, injectScheduler.test.js – mutations/state/scheduler.
      • activityLog.test.js · activityLogRoutes.test.js – structure des entrées, troncature IP, contrôle d'accès.
      • storeRepository.test.js, multiExerciseStore.test.js, storeBroadcastChannel.test.js – Cosmos enabled/disabled, migration, fallbacks, pub/sub par canal.
      • tenancyE2e.test.js, exerciseAuthz.test.js, eventBusChannels.test.js – cloisonnement par exercice (WU5/WU6).

      Suites retirées en WU11 (testaient des routes/modules Phase 1 retirés) : iamAuth, iamMfa, iamHardening, iamE2e, iamUsersStore, appPasswords, logout, plus les redondances couvertes par WU12 E2E (iamWebauthnLogin/Register/Recovery, iamAdmin, iamAdminResetCredentials, iamAccessLinksPasswordless, iamSessionsPasswordless, iamSecurityWebauthnKpi).

      Conventions : supertest pour les tests d'intégration HTTP, mocks Anthropic/Twister par stubs locaux. Pas de couverture mesurée à ce jour.

      10. Conventions

      • Style JS : ES2022, const par défaut, fonctions nommées, pas de TypeScript dans le code (mais tsconfig.json pour JSDoc/IDE).
      • Erreurs : pas de classes d'erreur custom - on renvoie un statut HTTP + JSON. Les exceptions imprévues sont attrapées par un middleware error handler.
      • Logging : logger.info / warn / error, jamais console.log en code applicatif.
      • Commits : style minuscule, type entre parenthèses ou tag docs:, ui(brand):, feat:, fix:. Voir git log.
      • Branche principale : main.

      11. Comment ajouter…

      Une nouvelle route HTTP

      1. Créer src/routes/maRoute.js avec un express.Router().
      2. Définir un schéma zod pour le body si POST.
      3. Appliquer les middlewares trainerAuth ou requireAccess([…]) selon les besoins.
      4. Monter dans src/app.js avec le préfixe /api/ma-route.
      5. Ajouter un test dans src/__tests__/.

      Un nouveau type de publication

      1. Étendre l'enum type dans src/routes/feed.js (zod) et dans le schéma scenario.
      2. Ajouter le rendu correspondant dans public/js/wall.js (nouvelle fonction myCard()).
      3. Ajouter le composeur dans public/js/newsroom.js + le formulaire HTML.
      4. Mettre à jour la timeline observateur (_obsItemDisplay).

      Une nouvelle source live

      1. Ajouter une clé dans store.sources (zod schema).
      2. Créer la route de fetch (src/routes/maSource.js).
      3. Ajouter le polling client dans app.js (intervalle dédié).
      4. Ajouter le rendu côté wall.js avec le badge EN DIRECT.

      Une nouvelle vague de réactions

      1. Étendre le validateur tone dans src/routes/inject.js.
      2. Ajouter le bloc de prompt correspondant.
      3. Ajouter un bouton dans la timeline animateur (newsroom.js).
      4. Définir un mapping de tone dans wall.js et reputation.js.

      Une traduction

      Pas d'i18n actuellement (UI 100 % française). Pour l'introduire : extraire les chaînes en dictionnaires, choisir une lib (i18next) ou un mécanisme léger maison.

      12. Build et assets

      • Pas de build step côté frontend - les fichiers JS/CSS sont servis tels quels depuis public/.
      • Cache-busting manuel : suffixe ?v=YYYYMMDDx sur chaque <script> et <link> de index.html. À bumper dès qu'un asset est modifié.
      • Service statique : express.static('public') avec Cache-Control: no-store en dev, etag désactivé.

      13. Performance

      • Mur joueur : rendu incrémental (patchFeed) - seuls les nouveaux items sont insérés en tête, pas de re-render complet.
      • Stats croissantes : un seul setInterval à 1.8 s pour tout le mur, avec décroissance par âge (multiplier ×0.04 au-delà de 60 min).
      • RSS : cache serveur 60 s, fetch parallèles via Promise.allSettled.
      • Cosmos : 1 document par exercice + 1 document partagé ; seuls les exercices modifiés sont réécrits (dirty-tracking, WU3).
      • Pagination : timeline animateur paginée (15 / page). Le mur joueur n'est pas paginé (rolling display).

      14. Comment debugger

      • npm run dev : nodemon + pino-pretty (logs colorés horodatés).
      • node --inspect src/server.js : breakpoints via Chrome DevTools.
      • Côté client : DevTools > Network > EventStream pour voir le flux SSE en clair.
      • Mocks Anthropic/Twister dans les tests : stubs des modules (Jest).
      • /api/status : aperçu rapide de l'état.
      • localStorage.setItem('pulseTrainerToken', '...') : injecte automatiquement X-Trainer-Token sur toutes les requêtes (utile en tests manuels).

      15. Contribuer

      1. Brancher depuis main (git checkout -b feat/ma-feature).
      2. Coder + ajouter des tests + bump du ?v= sur les assets touchés.
      3. npm test doit passer.
      4. Mettre à jour CHANGELOG.md sous une nouvelle entrée si user-visible.
      5. PR vers main, revue par un mainteneur.
      6. Style des messages de commit : voir git log récent.

      Pour les changements visuels (UI, branding), capturer un avant/après dans la PR.

      16. Glossaire technique

      SSE
      Server-Sent Events - flux HTTP unidirectionnel serveur → client. Utilisé pour pousser feed/state/messages.
      Inject
      Publication programmée dans le scénario de l'exercice, avec un offset T+ minutes.
      Tone
      Catégorie de sentiment : very_negative · negative · neutral · positive · very_positive.
      Trainer auth
      Garde des routes mutantes /api : rôle facilitator/admin ou X-Trainer-Token.
      access_token
      Cookie JWT HS256 { exerciseId, role, jti, exp }. Délivré uniquement par /access?token=… (redemption d'un lien JWT d'exercice).
      iam_sid
      Cookie opaque de session serveur nominative (admin/sysadmin), ouvert par WebAuthn → lookup store ; révocable, TTL/inactivité par rôle.
      Passkey
      Identifiant FIDO2 / WebAuthn – clé asymétrique générée par l'authentificateur (Touch ID, Windows Hello, clé USB, etc.) ; PULSE stocke la clé publique + métadonnées, jamais la clé privée. RP-ID = secalys.fr (D13).
      RP-ID
      Relying Party Identifier WebAuthn : domaine auquel la passkey est liée. Une passkey enrôlée sous secalys.fr fonctionne pour tout sous-domaine, mais pas si on change de RP-ID (cf. IAM_WEBAUTHN_RP_ID).
      UV (User Verification)
      Vérification utilisateur côté authentificateur (biométrie / PIN). Exigée par PULSE – c'est ce qui sert de 2ᵉ facteur (D3′), il n'y a plus de TOTP.
      enrolToken
      Jeton d'enrôlement usage-unique (TTL IAM_ASSISTED_GRANT_TTL_MIN min) émis par POST /iam/bootstrap, création de compte, reset-credentials ou recovery/redeem. Permet d'enrôler une passkey via /iam/webauthn/register/* sans session.
      Codes de récupération
      10 codes (par défaut, IAM_RECOVERY_CODE_COUNT) générés sur demande, hashés SHA-256 au repos, affichés une seule fois. À conserver hors-bande. Redeem public anti-énumération (réponse générique en cas d'échec).
      memoryStore
      Objet en mémoire centralisant tout l'état serveur. Persisté à chaque mutation vers Cosmos puis disque.
      Web search tool
      Outil Anthropic web_search_20250305, activé en beta pour la recherche d'images et l'enrichissement d'injects.
      SSRF guard
      Module qui résout les DNS et refuse les IPs privées avant fetch côté serveur (route /api/preview).
      Cache-bust
      Suffixe ?v=YYYYMMDDx sur les assets pour forcer le rechargement client.