Skip to content

Architecture

github-actions[bot] edited this page Oct 20, 2026 · 25 revisions

Architecture

Cette page décrit comment l'application est construite, pour un développeur qui reprend le code. Le contrat d'API est décrit dans API, l'installation dans Installer et les conventions de développement dans Contribuer.

Vue d'ensemble

flowchart LR
    nav([Navigateur]) -- HTTPS --> proxy[Proxy inverse<br/>IIS, nginx, Traefik…]
    proxy -- "HTTP :8080" --> app
    subgraph app ["Conteneur « app » (ASP.NET Core, .NET 10)"]
        api["/api/… : API JSON + flux SSE"]
        web["/… : front React (wwwroot)"]
    end
    app --> db[(PostgreSQL 16<br/>conteneur « db »)]
    app --> pj[/volume « pieces-jointes »/]
    app --> cles[/volume « cles »<br/>clés du cookie de session/]
    app --> sauv[/volume « sauvegardes »<br/>copies avant mise à jour ou import/]
Loading
  • Une seule image Docker contient l'API et le front déjà construit : l'API sert les fichiers statiques et renvoie index.html pour toute route qui n'est pas sous /api.
  • PostgreSQL est la seule dépendance externe. Le schéma est migré automatiquement au démarrage (EF Core), après une sauvegarde automatique contrôlée (voir Exploiter).
  • Pièces jointes : fichiers sur disque (volume Docker), métadonnées en base.
  • Instance unique : le flux temps réel, les verrous d'édition des étapes, les limites de connexion et les réservations des plafonds de pièces jointes sont tenus en mémoire, et les migrations tournent au démarrage. Pour plusieurs instances, il faudrait remplacer DiffuseurMep par LISTEN/NOTIFY PostgreSQL et sortir les migrations du démarrage.

Back-end

Trois projets dans backend/src, testés par backend/tests/GestionMep.Tests.

GestionMep.Domain : modèle et règles métier

Aucune dépendance vers ASP.NET ni EF Core.

  • Modele.cs : entités (Mep, Etape, Participant, Systeme, ResolutionType, FicheRecette, Approbation…) et énumérations.
  • Regles.cs et Regles.*.cs : toutes les règles, en fonctions statiques pures d'une même classe partielle, un fichier par domaine (Regles.Graphe.cs, Regles.StructurePlan.cs, Regles.Sections.cs et Regles.OptimisationLiens.cs (réduction transitive : liens inutiles, ordre d'exécution vérifié inchangé) pour le graphe libre et les sections, Regles.Disponibilite.cs, Regles.Droits.cs, Regles.Phases.cs, Regles.CycleDeVie.cs (annulation, report, retour en exécution, étapes correctives, changement de pilote, suppression), Regles.Recette.cs, Regles.Resolutions.cs, Regles.Bibliotheques.cs, Regles.TypeMep.cs, Regles.Suivi.cs (plan suivi, plus long chemin, fin estimée et retard, avancement)…). Elles couvrent la disponibilité d'une étape, la validité du graphe, la vérification du plan, les transitions de phase, les approbations, le go / no-go, le point de non-retour, la recette, le retour arrière, les résolutions types, les étapes modèles et les compteurs d'utilisations des bibliothèques, la duplication et le type de MEP (complète ou simplifiée : suppléants implicites, résolutions proposées le jour J, changements de type).
  • Un refus lève RegleMetierException avec un message en français destiné à l'utilisateur. L'API le traduit en 422, sauf ses deux sous-classes : DroitInsuffisantException (la personne n'a pas le droit : 403) et SaisieInvalideException (valeur vide, inconnue, en double : 400).
  • Fuseaux.cs : fuseaux horaires des MEP (identifiants IANA, identifiants Windows convertis), contrôle de la saisie et toutes les conversions entre instants et heures locales, changements d'heure compris.
  • L'heure courante est toujours passée en paramètre (maintenant) : les règles se testent sans horloge.
  • AnnuairePersonnes.cs : comptes vus comme des personnes. Des comptes reliés (Utilisateur.ComptePrincipalId, un seul niveau) forment une personne, nommée d'après son compte principal, dont les équipes sont celles de tous ses comptes. Rien n'est fusionné : chaque compte garde ses données, ses droits et son historique. Les règles de liaison et de nom d'équipe sont dans Regles.Comptes.cs.
  • TextesCommunication.cs : modèles de texte. VariablesTexte (catalogue des variables {{cle}} et leurs valeurs pour une MEP, dates et heures dans le fuseau de la MEP) et ModelesTexteFournis (les quatre modèles copiés dans chaque équipe, rétablissement). Le remplissage et la mise en forme (gras, puces, HTML copié) se font dans le navigateur (frontend/src/textes). Voir Modèles de texte.
  • DonneesDemo.cs : jeu de démonstration (comptes alice, bob, claire, MEP d'exemple, équipes et leurs modèles de texte).

GestionMep.Tableur : classeurs Excel

Brique commune, sans règle métier, pour écrire et relire des classeurs .xlsx, qui donne à tous les fichiers la même présentation :

  • ClasseurExcel : identité du classeur (InfosClasseur : titre, application, version, date de génération), propriétés du fichier, mise en page de chaque onglet (quadrillage masqué, A4 paysage ajusté à la largeur, en-tête avec le titre et l'onglet, pied de page « Généré par … » et « Page n / total »), onglet masqué « Listes » des listes déroulantes (noms définis Liste_…), feuille de contrôle masquée, structure verrouillée ; à l'enregistrement, premier onglet actif, curseur en A1 et formules calculées ;
  • FeuilleExcel : bloc de titre (titre, contexte, mention de génération), Tableau (vrai tableau Excel avec boutons de filtre : colonnes typées ColonneTableau — texte, code, nombre, date, durée —, colonnes facultatives vides retirées, identifiants masqués, notes sur les en-têtes, états colorés par mise en forme conditionnelle, en-têtes figés et répétés à l'impression, hauteur des lignes ajustée au texte), Fiche (libellé / valeur), listes déroulantes, cases à saisir, liens internes, protection ;
  • FeuilleAide : onglet « À lire » des classeurs à remplir (étapes, légende des cases jaunes, exemples grisés) ;
  • PaletteExcel et StylesExcel : couleurs injectables (primaire pour les en-têtes de tableau, avec un texte blanc ou foncé choisi par contraste ; secondaire pour les titres ; tertiaire pour les intertitres ; neutres et couleurs des états), PaletteExcel.Defaut reprenant celles de l'application ; formats communs (dates jj/mm/aaaa, durées h mm en fraction de jour) ;
  • LecteurClasseur : ouverture contrôlée d'un fichier envoyé (archive vérifiée et taille décompressée plafonnée avant lecture), onglets retrouvés sans tenir compte de la casse ni des accents, ligne d'en-têtes et colonnes repérées par leur nom (EnTetes, dans n'importe quel ordre), lecture des cellules et de la feuille de contrôle. Elle s'appuie sur ClosedXML (licence MIT), la bibliothèque libre de référence en .NET pour produire des classeurs mis en forme sans Excel installé. EPPlus a été écarté : sa licence (Polyform) n'est pas libre pour un usage commercial. La dépendance SixLabors.Fonts de ClosedXML est figée en 1.0.1 (Apache-2.0), les versions 2.x passant sous une licence non permissive. Elle sert à l'export et à l'import de la recette (Services/RecetteExcel.cs) et des objets impactés (Services/ObjetsExcel.cs), et aux exports en lecture des autres vues (Services/VuesExcel.cs).

GestionMep.Api : API HTTP

Dossier Rôle
Program.cs Composition : services, middlewares, montage des groupes de routes, amorçage. En développement seulement, Developpement:LatenceMs (> 0) retarde chaque réponse de l'API, pour voir l'application comme avec une base lointaine.
Endpoints/ Une classe statique par domaine (MapMeps, MapEtapes, MapPhase…), en Minimal API. Toutes les routes sont sous /api et exigent une session, sauf la connexion, les sondes /api/sante (vivacité) et /api/sante/prete (disponibilité : base, schéma, surveillance des échéances ; Services/SantePrete.cs) et /api/version.
Cli/ Commande gestion-mep (LigneDeCommande*.cs) : administration et exploitation sans navigateur. Elle appelle les opérations publiques des classes Endpoints/ (AdminEndpoints.Modifier, CatalogueEndpoints.Supprimer, ResolutionsEndpoints.Appliquer…), partagées avec les routes de la console : mêmes règles, même journal d'administration (acteur absent : auteur « Ligne de commande »).
Services/ServiceMep.cs Chargement d'une MEP (avec application de l'échéance de retour arrière), de plusieurs MEP en un nombre fixe de requêtes (ChargerPlusieurs, VersDtos : suivi en direct), écritures sérialisées par MEP (Modifier), contrôle des révisions, journal d'audit, enregistrement et notification ciblée.
Services/BlocsMep.cs Blocs de formulaire d'une MEP (intervention, échéance, sauvegarde, plan de retour arrière, communication, responsables techniques, approbateurs) et leurs révisions : empreinte de chaque bloc relevée à la lecture, révision augmentée à l'enregistrement si l'empreinte a changé, quelle que soit la route.
Services/Personnes.cs Chargement de l'annuaire des personnes (comptes reliés, équipes) ; noms affichés de tous les comptes (celui de la personne) pour les DTO des MEP, pièces jointes et résolutions types. La session relit aussi le nom de la personne à chaque requête : un compte relié signe les journaux de ce nom, avec son propre login.
Services/MepMapping.cs Seul endroit où une entité devient un DTO : calcul des disponibilités, des erreurs du plan et des droits de l'utilisateur courant.
Services/Dtos.cs Contrats d'entrée (…Saisie) et de sortie (…Dto).
Services/DocumentationOpenApi.cs Description OpenAPI servie sur /api/openapi.json, copiée dans docs/openapi.json pour générer les types du front.
Services/ErreurHttp.cs Erreurs HTTP explicites et middleware qui traduit les exceptions en { "message": "…" }.
Services/DiffuseurMep.cs Diffusion en mémoire aux abonnés du flux SSE : dernière version de chaque MEP, présence (personnes connectées au flux, éléments déclarés par chaque onglet) et verrous d'édition des étapes (événement verrous, expiration après une minute sans renouvellement, TimeProvider pour les tests). La présence et les verrous vivent avec la connexion : le dernier flux d'un onglet fermé les retire. Le tableau de bord « Suivi en direct » s'abonne à une pseudo-MEP (DiffuseurMep.Suivi, identifiant vide) que chaque notification réveille aussi.
Services/SimulationDemo.cs Mode démonstration : « Simuler des collègues ». Bob et Claire ouvrent des abonnements virtuels au flux de « Facturation électronique » (présence réelle) et y agissent par les règles et services habituels (verrous, modifications, ajout et déplacement d'étape, prise de main), au journal ; arrêt au bouton, au délai (Demo:SimulationDureeS, 180 s) ou à la réinitialisation, avec nettoyage (verrous, présence, étapes de passage). Rythme : Demo:SimulationPasMs (3,5 s).
Services/SurveillanceEcheances.cs Tâche de fond : chaque minute, bascule en retour arrière les MEP dont l'échéance est atteinte (plan de retour arrière activé).
Services/StockagePiecesJointes.cs Écriture, lecture et suppression des fichiers joints ; fichiers sans enregistrement en base mis de côté au démarrage.
Services/MisesAJour.cs Version en cours, vérification des versions publiées sur GitHub et dialogue avec le service facultatif de mise à jour (fichiers dans un volume partagé).
Services/Sauvegardes.cs Archive de la base (COPY binaire par table) et des pièces jointes ; sauvegarde avant migration ou importation, contrôle qu'aucune donnée n'est perdue (les tables reprises par une migration, déclarées dans Reprises, sont comptées avec leur table d'arrivée), puis suppression de la sauvegarde.
Services/ExcelCommun.cs Ce que partagent les classeurs de l'application : création (titre, version, date dans le fuseau de la MEP, palette), noms de fichier, tonalités des états (mêmes couleurs que les pastilles de l'interface).
Services/RecetteExcel.cs Export de la recette d'une MEP en classeur Excel (« À lire », sommaire avec compteurs calculés par formule, un tableau par système avec une ligne par étape et sa référence masquée, listes et contrôle masqués) et relecture du fichier rempli (lignes retrouvées par leur référence, colonnes par leur en-tête ; le format précédent « recette-1 » reste accepté), avant l'analyse de la validation par les règles du domaine. Services/BlocsMarkdown.cs découpe les blocs Texte / Code d'un déroulé, comme le front.
Services/ObjetsExcel.cs Modèle, export et import des objets impactés (« À lire », saisie avec listes déroulantes et contrôle au catalogue par formule, catalogue, listes masquées) ; l'import repère les colonnes par leur en-tête et accepte aussi les fichiers de la version précédente.
Services/VuesExcel.cs Exports Excel en lecture des vues (liste des MEP, préparation, étapes, retour arrière, journal), à partir des DTO : un tableau ou une fiche par onglet, mêmes libellés et mêmes filtres que l'interface. Routes dans Endpoints/ExportsExcelEndpoints.cs, partagées avec gestion-mep meps lister --excel et meps excel.
Services/RapportsFin.cs Rapport de fin : instantané d'une MEP clôturée (DTO de la MEP sans droits, journal complet, synthèse et points bloquants calculés par Calculer, tests unitaires) sérialisé dans la table RapportsFin (Contenu en jsonb, Format pour une évolution du contenu). Figé juste après la clôture par POST …/cloturer (une erreur d'écriture est journalisée sans annuler la clôture) ; une MEP clôturée avant son existence obtient le sien à la première lecture (CalculeApres). Services/VuesExcel.Rapport.cs en fait le classeur ; routes dans Endpoints/SuiviEndpoints.cs, avec le tableau de bord GET /api/suivi et son flux.
Services/CharteVisuelle.cs, Services/ServiceCharte.cs Charte visuelle. CharteVisuelle fait foi pour les calculs : lecture et validation du fichier charte.json (format version 1, erreurs et avertissements numérotés par ligne), thème sombre et jetons dérivés des couleurs, contrôles de lisibilité et corrections proposées, feuille /charte.css (seulement les jetons qui diffèrent de index.css), PaletteExcel des exports. Le front refait les mêmes calculs pour l'aperçu de la console ; les cas de frontend/src/charte/vecteurs-charte.json et la comparaison avec index.css sont joués des deux côtés. ServiceCharte garde la charte en vigueur : console (réglage Charte:Console de la table Reglages) > fichier (Charte:Fichier, variable CHARTE_FICHIER) > défaut, relue toutes les 10 secondes (plusieurs instances, fichier modifié) ; son empreinte sert d'ETag à /charte.css et est renvoyée par /api/version. Routes dans Endpoints/CharteEndpoints.cs, commandes gestion-mep charte ….
Services/EchangeResolutions.cs Format JSON d'export des résolutions types et importation contrôlée fiche par fiche (gestion-mep resolutions exporter / importer).
Securite/ Protections HTTP : en-têtes du proxy inverse, limite des tentatives de connexion (par IP et par compte), en-tête anti-CSRF exigé, en-têtes de sécurité (CSP…), taille maximale des requêtes (l'empreinte CSP du script de index.html est calculée après avoir ramené les fins de ligne à LF, comme le navigateur), sondes --sante et --sante --prete de l'image Docker et des scripts d'installation.
Auth/ Comptes locaux, cookie de session, Acteur (utilisateur courant), connexion unique OpenID Connect (ConnexionUnique.cs).
Donnees/ MepDbContext, migrations EF Core, Amorcage (migration précédée d'une sauvegarde, démonstration ou compte administrateur initial).

Compteurs d'utilisations des bibliothèques

Étapes modèles, modèles de recette et résolutions types portent un compteur Utilisations (nombre de MEP qui les utilisent) stocké en base, et une date de création. Chaque MEP garde la liste des éléments déjà comptés (Mep.ElementsBibliothequeComptes) : à chaque enregistrement, ServiceMep compare les éléments utilisés par la MEP (étapes copiées d'un modèle, fiches copiées d'un modèle de recette, résolutions liées) à cette liste, note les nouveaux et, après l'enregistrement, augmente leurs compteurs d'un par une mise à jour atomique en base. Une MEP compte donc chaque élément une seule fois, même retiré puis remis, même si l'écriture est rejouée ; un modèle de MEP ne compte pas. Supprimer une copie ne décompte pas.

Déroulé d'une requête d'écriture

  1. Le handler passe par ServiceMep.Modifier, qui charge la MEP par ServiceMep.Charger (celui-ci applique d'abord l'échéance de retour arrière si elle est dépassée) puis exécute les étapes 2 à 4, rejouables en cas d'écriture simultanée (voir Concurrence).
  2. Il vérifie les droits (ServiceMep.ExigerEditeur, Regles.EstPilote…) et la révision envoyée par le client (ExigerRevision : de l'étape, de la section, du bloc ou de la sauvegarde d'un système ; ExigerVersion là où la version de la MEP reste demandée).
  3. Il appelle une ou plusieurs fonctions de Regles, qui modifient l'entité ou lèvent RegleMetierException.
  4. ServiceMep.Enregistrer augmente les révisions des blocs changés et la version, ajoute l'événement au journal, enregistre ; la notification du flux SSE part après la validation de la transaction, avec ce qu'elle change pour le suivi en direct (PorteeSuivi : rien, la carte de la MEP lancée, ou les listes).
  5. La réponse est la MEP complète (MepDto), recalculée pour l'utilisateur courant. Le front remplace son état par cette réponse.

Concurrence

  • Chaque étape et chaque section portent une Revision, chaque bloc de formulaire de la MEP la sienne (Mep.Revisions, type possédé : colonnes Revisions_* de Meps), chaque système RevisionSauvegarde, et la MEP une Version qui augmente à chaque écriture. Le client renvoie la révision de ce qu'il modifie ; un écart donne 409, avec un message qui nomme la partie modifiée, et le front relit la MEP sans toucher aux brouillons. Une écriture sur un autre bloc ou une autre étape ne périme pas la révision (#96).
  • Écritures sérialisées par MEP : ServiceMep.Modifier ouvre une transaction et prend le verrou de la ligne de la MEP (SELECT … FOR UPDATE, attente bornée par lock_timeout à 15 secondes, puis 409 « réessayez dans un instant ») avant de la relire : deux écritures sur la même MEP passent l'une après l'autre, la seconde voit ce qu'a écrit la première. Dix personnes qui agissent en même temps sur des étapes ou des blocs différents réussissent toutes (tests ConcurrenceTests, contrôle de charge ChargeTests).
  • EF Core utilise aussi la Version de la MEP (celle des résolutions types, des modèles de recette et des étapes modèles) comme jeton de concurrence. Si l'enregistrement échoue malgré tout (jeton, clé unique prise entre-temps, interblocage), le serveur vide le suivi EF (ChangeTracker.Clear()), relit la MEP, revérifie toutes les préconditions et rejoue l'action, jusqu'à 8 essais espacés de façon croissante et aléatoire.
  • Ce qui reste un vrai conflit (409) : une étape, une section ou un bloc modifié depuis sa lecture (révision périmée, par exemple deux démarrages simultanés de la même étape : un seul aboutit), une MEP modifiée depuis sa lecture sur les routes qui exigent encore sa version (changement de type, décision d'approbation, saisies d'anciens scripts), ou une MEP restée verrouillée plus de 15 secondes.
  • Une action rejouée ne doit pas avoir d'effet hors base : le journal et les entités ajoutés sont annulés par ChangeTracker.Clear(), la notification SSE n'est envoyée qu'après un enregistrement réussi, le fichier d'une pièce jointe est écrit une seule fois avant l'enregistrement (et effacé s'il échoue définitivement), les fichiers supprimés ne le sont qu'après le succès.

Codes d'erreur

Les règles de choix des codes (400 saisie, 403 droits, 422 règle métier…) et le format des erreurs (problem details, application/problem+json) sont décrits dans API. Côté serveur : ErreurHttp (erreurs propres à l'API : Saisie, Interdit, NonTrouve, Regle…), les exceptions du domaine ci-dessus, et ReponsesErreur, qui écrit le corps de toutes les erreurs (intergiciel, refus d'authentification, limiteur de débit, protection CSRF, route inconnue). Une route réservée aux administrateurs se déclare par .ExigerAdmin(), jamais par un test dans son code.

Base de données

  • PostgreSQL via Npgsql. Les listes et petits objets attachés à une entité (dépendances, systèmes d'une étape, responsables techniques, expertises, sections du plan et leurs cercles…) sont stockés en jsonb via AsJson / AsJsonObjet (MepDbContext.cs).
  • RapportsFin garde un rapport de fin par MEP clôturée (clé : la MEP, suppression en cascade), son contenu en jsonb.
  • Les migrations sont dans Donnees/Migrations. Voir Contribuer pour en créer une.

Authentification

  • Comptes locaux, mots de passe hachés par PasswordHasher (PBKDF2), cookie HttpOnly SameSite=Lax, Secure hors développement. Les clés qui chiffrent le cookie sont conservées dans un volume (Cles:Dossier) : un redéploiement ne déconnecte personne.
  • Connexion limitée par adresse IP (fenêtre d'une minute) ; LimiteurConnexion bloque un login quelques minutes depuis une adresse après 5 échecs consécutifs, signale 20 échecs en une heure sur un login (journal d'administration), borne sa mémoire et le nombre de hachages simultanés. Login et mot de passe démesurés sont refusés sans calcul.
  • Ressources bornées : QuotasPiecesJointes (total par MEP, dépôts d'un compte sur 24 heures, espace disque minimal, réservations en mémoire pour les dépôts simultanés ; un dépôt est reçu en flux par MultipartReader, sans tampon : refusé d'après sa taille annoncée avant d'être lu, puis arrêté dès qu'il dépasse la place restante, StockagePiecesJointes.Ecrire) ; DiffuseurMep (flux temps réel par compte et pour le serveur, renouvelés toutes les heures).
  • Toute requête /api qui modifie doit porter l'en-tête X-Requested-With (protection CSRF) ; le front l'ajoute toujours.
  • Toutes les réponses portent une politique de contenu (CSP) qui interdit l'affichage dans un cadre, ainsi que nosniff et Referrer-Policy. Les corps de requête sont limités à 1 Mo, sauf sur les routes de dépôt de pièces jointes.
  • Le reste de l'API ne lit que les claims NameIdentifier, Name et Role via Acteur.Depuis(user).
  • Connexion unique facultative (Auth/ConnexionUnique.cs) : gestionnaire OpenID Connect d'ASP.NET Core (code + PKCE, retour sur /connexion-sso/retour). Ses réglages sont une ligne de la table ConfigurationsSso, modifiée par la console ou gestion-mep sso ; ReglagesSso la relit au départ vers le fournisseur et au retour, et vide le cache des options OpenID Connect quand elle a changé : pas de redémarrage. Le secret est chiffré par Data Protection (mêmes clés que le cookie) et effacé quand l'émetteur ou l'ID d'application change, pour qu'il ne parte jamais vers un autre fournisseur ; le test refuse un point de jeton hors https (et hors login.microsoftonline.com pour Entra ID) et ne suit pas les redirections. Au retour, ConnexionUnique.Resoudre retrouve le compte par émetteur + oid/sub (table IdentitesExternes), le relie (liaison depuis le profil, jeton à durée limitée propre à la session) ou le crée, puis émet le même cookie qu'une connexion locale.

Front-end

React 19 + TypeScript 7 + Vite, Tailwind CSS 4 et primitives Radix. Le détail des commandes et des dossiers est dans frontend/README.md.

  • Une page par route (src/pages, src/parametres), les onglets d'une MEP dans src/mep.
  • La logique pure (graphe, disponibilité, recherche, blocs d'instructions, formats) est isolée dans des modules testés sans DOM : graphe.ts, domaine.ts, recherche.ts, blocs.ts, format.ts.
  • Graphe des étapes (mep/graphe) : rendu par React Flow (@xyflow/react), rangement automatique par ELK (elkjs, chargé à la demande et exécuté dans un Web Worker par moteurElk.ts : délai de 15 secondes, moteur oublié et recréé après un échec, message « Rangement automatique indisponible » avec « Réessayer » (AvisRangement.tsx) ; un fichier disparu après un déploiement (vite:preloadError) affiche le bandeau de nouvelle version), qui trace aussi les liens (routage orthogonal : ils contournent cartes, cadres, titres et cercles). rangementBordure.ts (rangerBordure) range de bas en haut : chaque section dépliée est rangée à part (SEPARATE_CHILDREN, placement NETWORK_SIMPLEX), puis le niveau qui la contient. Les cercles d'entrée et de sortie d'une section dépliée sont des ports posés sur sa bordure (point sur le bord, étiquette à l'intérieur, comme sur une section repliée) et son titre est une étiquette sur la bordure haute (repli, édition, présents) ; un lien direct qui franchit une bordure passe par un port implicite. Les liens sont fusionnés des deux côtés par groupes complets : les liens d'une même source partagent leur tronc de départ, ceux d'une même cible leur tronc d'arrivée, sans jamais faire croire à une relation qui n'existe pas. routage.ts compte croisements et superpositions (compterCroisements, mesure des réglages) ; routageBordure.ts (sautsSansJonctions) dessine les tracés (coudes arrondis, petit saut d'un segment horizontal là où il croise le segment vertical d'un autre lien, mais aucun sur un tronc partagé ni à une jonction). La vue d'une section utilise le même rangement (ses cercles y sont des nœuds). modeleGraphe.ts construit nœuds et liens (étapes, sections repliées ou dépliées, cercles d'entrée et de sortie), rangerBordure les place (rangement.ts garde les tailles et l'ancien ranger, qui ne sert plus qu'à ses tests), GrapheLibre.tsx ne montre jamais un graphe non rangé : jusqu'au premier rangement fait avec les tailles mesurées et la vue d'arrivée posée, un squelette (SqueletteGraphe.tsx) tient la place, puis le graphe paraît en fondu ; même chose au passage du plan à la vue d'une section et retour (l'ancienne vue disparaît aussitôt, jamais montrée sous l'en-tête de la suivante). Repli et dépli d'une section (plan, retour arrière, vue d'ensemble, exécution, « Tout déplier / Tout replier ») et étape ajoutée ou reçue d'un collègue : l'enchaînement suit l'état du rangement et des mesures, jamais un délai fixe. 1. Les nœuds qui vont paraître et ne sont pas encore mesurés sont rendus en copies invisibles (mesuresGraphe.ts, nœuds mesure:… posés dans un nœud affiché, hors mini-carte) ; useRangement (attendreMesures) attend leurs tailles et ne fait qu'un rangement, définitif. D'ici là, le graphe affiché ne bouge pas, liens compris (ils suivent le rangement affiché), et le bouton de la section signale l'attente par une pastille qui tourne, après un court délai. 2. Les nouveaux nœuds sont montés invisibles ; le mouvement part quand React Flow les a tous mesurés. 3. Un seul mouvement (mouvementGraphe.ts), entièrement calculé avant sa première image, d'environ une demi-seconde (DUREE_MOUVEMENT_MS, 480 ms) avec un départ et une arrivée en douceur (adoucir, sinusoïde : pas l'essentiel du chemin dans les premières images, le changement se lit comme un mouvement et non comme un saut) : chaque nœud va de sa boîte affichée à sa boîte rangée ; le cadre qui s'ouvre ou se ferme grandit ou rapetisse et son intérieur se dévoile ou se cache avec lui (étapes à leur taille réelle, ancrées au milieu de son bord haut et découpées par le cadre (decoupe, clip-path), révélées rangée par rangée en glissant un peu (revelation) ; les liens intérieurs sont découpés de même), son titre et ses cercles de bordure restent attachés à lui ; le chevron de la section tourne dès le clic ; le reste glisse et la vue suit (une section dépliée est montrée en entier dans ce même mouvement). Avec « réduire les animations » du système, le graphe passe directement à l'arrivée. Pendant ce mouvement, les liens de React Flow sont masqués et le graphe dessine les mêmes liens attachés à leurs nœuds (LiensEnMouvement, tracé d'arrivée dont les bouts suivent les nœuds ; un lien qui change de bout passe en fondu de l'ancien tracé au nouveau) : aucun lien ne disparaît. À l'arrivée, les tracés d'ELK reprennent exactement la même place. Une section qui se replie reste un cadre jusqu'à l'arrivée, une section qui se déplie devient un cadre dès le départ. Tailles mesurées (tailleRetenue) : une étape à toute mesure, une section repliée seulement sa hauteur, affichée à sa largeur rangée et sans hauteur imposée ; sa largeur vient du rangement, jamais d'une mesure (sinon une mesure prise pendant un mouvement, par exemple interrompu par un second clic, resterait imposée à la section jusqu'au rechargement). La vue d'arrivée est posée après le rendu des nœuds rangés et mesurés. Jamais une image avec des cartes à moitié placées ou des liens détachés, même sur un poste lent (vérifié image par image avec le processeur ralenti six fois). useRangement.ts gèle le rangement tant qu'une étape est en saisie, GrapheLibre.tsx garde la carte regardée fixe à l'écran après un rangement et anime le reste ; l'ordre des éléments sœurs est stable : le rang choisi par les utilisateurs (rang, commun, PUT /api/meps/{id}/ordre), sinon un ordre barycentrique (trierEtapes : profondeur par profondeur, chaque étape au barycentre de ses préalables, ligne, ordre, code départageant) qui ne dépend que des liens ; ELK le respecte (forceNodeModelOrder, sans échange glouton), dans le plan comme dans une section dépliée. Les tracés ne captent pas le pointeur : survol, clic et cible d'un dépôt sont calculés sur les tracés d'ELK (liensAuPoint, tolérance de quelques pixels à l'écran), pour viser un lien précis là où plusieurs partagent un tronçon ; le menu d'un lien et les menus de création (MenusGraphe.tsx, étape vierge ou depuis un modèle) sont des menus Radix posés au point du clic. Dans le plan, l'intérieur d'une section dépliée est en lecture seule (nœuds enfants : ni glisser, ni poignées, ni « + ») ; il se modifie dans la vue de la section, dont le bandeau est rendu dans le cadre du graphe (plein écran tenu par EtapesEditeur). Le cadre du graphe tient dans la hauteur de la fenêtre (hauteurDisponible.ts : ce qui suit le graphe compte, et la correction de ce qui dépasse est gardée, pour qu'une barre de défilement qui apparaît puis disparaît ne fasse pas basculer la page sans fin ; plein écran possible). Sous 800 px de haut (petitEcran.ts), EtapesEditeur passe le plan en vue compacte : cadre à la hauteur de la fenêtre et page défilée jusqu'à lui, barre d'outils (BarreOutilsGraphe.tsx) et bandeau de section sur une ligne, étape ouverte en tiroir dans le cadre (EmplacementTiroir, portail Radix) ; sous 1440 px de large, le bac devient un rail et le menu latéral se replie le temps qu'une étape est ouverte (layout/menuLateral.ts) ; le bac (BacPlan, SectionsPlan.tsx : sections et étapes à placer) y est un panneau latéral avec son propre défilement, rendu dans le graphe (lateral) et relié à lui par ContexteBac : pendant le glisser d'une carte ou d'une section, le bac est une zone de dépôt (éclairé ; au-dessus, le défilement automatique de React Flow est coupé, autoPanOnNodeDrag), et une étape du bac se glisse dans le graphe par le même geste qu'une étape modèle (glisserCopie). Côté serveur, une étape au bac porte un indicateur explicite (Etape.APlacer) plutôt qu'un état déduit de « sans lien » : la première étape d'un plan, une étape posée à part ou un plan d'une seule étape n'ont aucun lien sans être à placer ; Regles.Operer garantit qu'une étape au bac n'a aucun lien (un lien reçu la replace). Verrou d'édition d'une étape (verrouEtape.tsx) : pris à l'ouverture, renouvelé, relâché à la fermeture ; ses changements arrivent par le flux temps réel (event: verrous). Une prise de main (verrou avec reprisDeNom) est annoncée à tous par MepPage (nouvellesPrisesDeMain). Cible d'un dépôt (cibleDepot.ts) : la géométrie (cartes, cercles, tracés) est relevée au début du geste, puis la cible est cherchée par calcul, une fois par image (requestAnimationFrame) : carte (après), cercle (après), lien le plus proche (zone d'accroche de 16 px à l'écran), dessous d'une carte (après) ou côté (en parallèle ; le côté d'une carte de sa propre rangée change seulement l'ordre) ; fantomePour dessine l'aperçu de la place et des liens à venir. La copie d'une étape modèle suit le pointeur par une transformation posée directement, sans rendu React, et les nœuds (TYPES_NOEUDS) ne se rendent pas à nouveau quand seule leur position change : le glisser reste fluide. « Optimiser les liens » (OptimisationLiens.tsx) montre en rouge les liens inutiles calculés par le serveur avant de les retirer.
  • Toute action passe par MepContext.run, qui appelle l'API, remplace la MEP par la réponse et gère les conflits.
  • Chargements (ui/Squelette.tsx, ui/Apparition.tsx) : tant que les données n'ont pas répondu, un squelette aux dimensions du contenu final tient sa place (silhouette de l'application le temps de lire la session et le mode démonstration, page d'une MEP, cadre du graphe, vue d'ensemble, suivi en direct et rapport de fin (mep/ensemble/Squelettes.tsx), listes, bibliothèques, journal, console) ; il n'apparaît qu'après un court délai, puis le contenu paraît en fondu. Chaque page chargée à la demande a sa propre limite Suspense (page() dans App.tsx, silhouette ChargementPage) : en changeant de page, la silhouette de la page demandée remplace aussitôt l'ancienne, qui ne reste pas figée à l'écran le temps de charger son code. Les boutons d'action passent leur traitement à Button par action : si elle renvoie une promesse, le bouton est occupé jusqu'à la réponse (roue à la place de l'icône, désactivé, aria-busy, un second clic ne relance rien) ; chargement reste disponible quand l'état vient d'ailleurs. Aucun état provisoire faux (« Aucun… », valeur par défaut) avant la réponse, ni après une erreur : un vide n'est affiché que si le serveur a répondu vide. Erreurs de chargement (ui/ErreurChargement.tsx) : trois formes (bloc, ligne dans une section, page entière avec « Retour à la liste »), un message selon la cause (réseau coupé, 502 à 504 pendant une mise à jour, 500, 403, 404 ; rien pour un 401, qui ouvre « Session expirée »), « Réessayer » branché sur le rechargement, et pour les administrateurs les détails techniques (requête, code, heure) à copier. Listes communes (referentiels.ts) : useReferentiel (catalogue, résolutions, comptes, équipes) et useDonneePartagee lisent une clé une seule fois pour tous les composants qui l'utilisent (requêtes regroupées, relue au retour après dix secondes) et exposent erreur et recharger ; l'onglet Préparation (mep/preparation/donnees.ts, usePreparation) attend ses listes puis révèle toutes ses sections d'un coup (Apparition), chacune montrant l'erreur de sa liste sans bloquer les autres. useAsync garde les données affichées pendant un rechargement (chargement, rechargement : la liste précédente reste, estompée). request compte les requêtes en cours ; ActiviteReseau (dans Shell) en montre un mince trait en haut de la fenêtre si la réponse se fait attendre, sauf pour les requêtes de fond (silencieux : présence, verrous, sondages périodiques). Les animations respectent prefers-reduced-motion (index.css, ui/mouvement.ts).
  • Onglets d'une MEP (mep/onglets.ts) : Vue d'ensemble (sauf modèles), Préparation, Plan (sous-onglets Étapes, Retour arrière, Recette, dans ?onglet=plan&plan=…), Exécution, Journal ; les anciennes adresses ?onglet=etapes|retour|recette sont réécrites. Onglet d'arrivée : pendant l'exécution et le retour arrière, la vue d'ensemble pour qui n'exécute aucune étape du plan en cours (executeLaMep), sinon l'exécution ou le plan.
  • Suivi (mep/ensemble) : calculs.ts (logique pure, testée) dérive tout de la MEP : états de la vue d'ensemble, étapes bloquées (qui attendent une étape en échec), plus long chemin et fin estimée (mêmes règles que Regles.EstimerFin côté serveur), ordre du tableau de bord, présence localisée. VueEnsembleTab.tsx réutilise GrapheLibre en lecture seule avec la prop ensemble (couleurs par état dans CarteEtape, NoeudsGraphe et LigneEnsemble.tsx, mini-carte masquée par défaut). pages/SuiviPage.tsx (tableau de bord, flux /api/suivi/flux : relit seulement les cartes des MEP lancées que le flux nomme, ou tout le suivi quand les listes changent, relectures regroupées) et pages/RapportPage.tsx (rapport figé, export Excel, PDF par l'impression du navigateur : menu et bandeau masqués par print:hidden).
  • Listes longues (#96) : la liste des MEP (pages/liste/listeMeps.ts, useListeMeps) demande ensemble une page de /api/meps/page et les comptes de la barre de filtres (/api/meps/facettes) : l'écran ne montre jamais une page sans ses comptes ; « Afficher plus » ajoute la page suivante sous des lignes de chargement, et la liste précédente reste affichée, estompée, pendant un changement de filtre. Côté serveur, FiltreMeps.Prefiltrer restreint la liste en base (type, phase, états, systèmes, équipes, période élargie d'un jour), puis la règle exacte partagée avec le navigateur s'applique aux résumés retenus. Le journal d'une MEP (mep/journal.ts) lit sa première page, puis à chaque version seulement les événements plus récents (apres) ; ses filtres sont appliqués par le serveur. L'onglet Journal le lit par useJournal (filtres, « Afficher plus ») ; la vue d'ensemble et les approbations de l'onglet Préparation (famille « approbation ») par useJournalMep (mep/preparation/donnees.ts), une donnée partagée lue une fois pour tout l'onglet dont le chargeur reçoit la dernière réponse gardée et n'en demande que la suite (lireJournal).
  • Temps réel : un EventSource par MEP ouverte reçoit la nouvelle version, et le front relit et affiche aussitôt la MEP si elle est plus récente que la sienne. Une saisie en cours n'est jamais écrasée : chaque formulaire garde son brouillon (useDraft) et signale un changement de sa section survenu pendant la saisie (AvisConflit).
  • Présence (presence.tsx) : le même flux reçoit la présence. Un composant déclare l'élément sur lequel on travaille (usePresenceElement, ou ZonePresence qui le déclare tant que le focus y est ou qu'une saisie y attend) ; MepPage regroupe les déclarations de l'onglet et les envoie (PUT …/presence), et les redéclare à chaque réouverture du flux. MarquePresence encadre un élément modifié par quelqu'un d'autre ; la couleur d'une personne (couleurPersonne) est celle de son avatar. Clés : etape:{id}, section:{id}… disent où l'on modifie ; ici:etape:{id} (fiche ouverte, même en lecture) et ici:section:{id} (vue d'une section) disent où l'on est : PresentsGraphe.tsx en tire les avatars posés sur les cartes et les sections, avec le détenteur du verrou.
  • Session expirée : un 401 (hors connexion et déconnexion) appelle le gestionnaire installé par auth.tsx, qui ouvre layout/SessionExpiree.tsx par-dessus la page, sans la démonter ; la requête attend la reconnexion puis repart une fois (api.ts, une seule attente pour toutes les requêtes). Changer de compte abandonne la saisie après confirmation et fait échouer les requêtes en attente. Avant une reconnexion Microsoft, les saisies (useReportDirty(dirty, libellé, brouillon)) et l'adresse de la page sont copiées dans sessionStorage (brouillons.ts) puis reprises au retour. Protege passe la page demandée à l'écran de connexion (state.from), qui y revient après l'identification.
  • Modifications non enregistrées : les formulaires se signalent par useReportDirty(dirty, libellé) ; GardeModifications (autour des pages connectées) retient toute navigation qui change de vue (autre page, onglet ou sous-onglet) avec useBlocker (routeur de données createBrowserRouter) et ouvre « Quitter sans enregistrer ? » avec la liste des libellés ; beforeunload couvre la fermeture et le rechargement.

Tests

Projet Contenu Commande
backend/tests Règles métier (sans base) et API de bout en bout sur une vraie base PostgreSQL (WebApplicationFactory) cd backend && dotnet test
frontend Logique pure et composants (Vitest + Testing Library, jsdom) cd frontend && npm test

Intégration continue

.github/workflows/ci.yml lance les tests back (avec un service PostgreSQL) et front à chaque PR et à chaque fusion sur main, ainsi que l'analyse CodeQL (C# et TypeScript, job codeql), l'audit des dépendances (npm audit --audit-level=high et dotnet list package --vulnerable, job dependances), ShellCheck, la syntaxe PowerShell et la validité du fichier Compose (job scripts), et, sur l'image construite sans publication, l'analyse Trivy (vulnérabilités critiques ou élevées corrigées) et un démarrage réel contre PostgreSQL jusqu'à la sonde /api/sante/prete (job image-controles, exigé avant la publication d'une version). Sur main, il construit et publie l'image sur ghcr.io/soladevsolide/gestion-mep (étiquettes main et sha-…). Une étiquette Git vX.Y.Z publie la version numérotée (X.Y.Z, X.Y, latest), puis crée la version GitHub avec ses notes, que l'indicateur de mise à jour de l'application consulte. Le job archives construit, à chaque PR comme à chaque version, les archives autonomes de l'installation sans Docker (.github/scripts/archives.sh : dotnet publish pour linux-x64 et win-x64, le front dans wwwroot, et les scripts du dossier installation/) ; elles sont jointes à la version GitHub, avec le docker-compose.yml et l'env.example de la version (images fixées à la version, sans construction locale). Le type d'installation (docker, linux ou windows, renvoyé par GET /api/application) adapte les commandes affichées par la console et par gestion-mep. Le service facultatif de mise à jour (mise-a-jour/, un script shell sur l'image Docker CLI) est publié avec les mêmes étiquettes sur ghcr.io/soladevsolide/gestion-mep-mise-a-jour.

Clone this wiki locally