Thème
Backend — service de supervision
Service Windows ASP.NET Core qui interroge les centrales incendie en Modbus, tient l'état du parc en mémoire, le persiste en PostgreSQL, l'expose en REST et le pousse en SignalR. Il sert également l'interface web depuis son propre wwwroot.
| Technologie | C# .NET 9, ASP.NET Core, EF Core 9 + Npgsql, SignalR, Serilog, NModbus |
| Point d'entrée | Program.cs — assemblage délégué à StartupExtensions.cs |
| Nom d'assembly | Superviseur-Service |
| Service Windows | Superviseur-Service (ServiceName de app.config.json) |
| Écoute | https://localhost:5000 en production, http://localhost:5264 en développement |
| Référence projet | dotnet/shared |
| Consommateurs | vue, dotnet/client, dotnet/manager, dotnet/tools/TsEnumGenerator |
Structure
| Dossier | Contenu |
|---|---|
Domain/Entities/ | Entités EF : Zone, User, SystemLog, AuditTrailEntry, BuildingMap, GlobalConfiguration, InisNetwork, OperationalNote, ReportTemplate, GeneratedReport, ModbusMetricSnapshot, PanelPointConfig, DemoSeedMarker |
Domain/ValueObjects/ | ModbusProfile, IncidentDefinition, CommandDefinition, SystemFlagDefinition, SystemStatusFlags, IpAddress, Port, ModbusConnectionSettings |
Domain/Models/ | FirePanel — la centrale telle qu'elle vit en mémoire — et SystemFlagState |
Domain/Profiles/ | PanelProfileRegistry (profils livrés), ModbusProfileValidator |
Domain/Enums/ | Énumérations métier, dont celles reprises en TypeScript |
Application/Services/ | Services métier : état des centrales, incidents, acquittement, audit, sauvegarde, rapports, statistiques |
Application/DTOs/ | Contrats transportés vers l'interface, dont PanelDto et PanelSnapshot |
Application/Interfaces/ | Abstractions consommées par la présentation et l'infrastructure |
Infrastructure/Adapters/ | Adaptateurs de centrale : InisAdapter, AvissAdapter, GenericModbusAdapter, base commune, GlobalCoilReader |
Infrastructure/Communication/ | Transport Modbus TCP et RTU, tube nommé, mise en cache et partage des connexions |
Infrastructure/BackgroundJobs/ | Six services hébergés (voir plus bas) |
Infrastructure/Persistence/ | FireSupervisorDbContext, DbInitializer, DevPostgresCluster, DemoDataSeeder |
Infrastructure/Repositories/ | Dépôts EF : générique, centrales, zones, journaux, audit |
Infrastructure/Crypto/ | InisConfigCrypto — déchiffrement des fichiers de configuration INIS |
Infrastructure/Telemetry/ | Collecte et interrogation des mesures de liaison Modbus |
Presentation/Controllers/ | 21 contrôleurs REST |
Presentation/Hubs/ | ModbusHub, unique hub SignalR |
Presentation/Filters/ | NotifiesResourceAttribute et ResourceChangeFilter |
Configuration/ | Chargement de app.config.json, protection des secrets, options typées |
Migrations/ | 44 migrations EF Core |
Resources/Fonts/ | Polices embarquées, utilisées par la génération PDF |
Uploads/ | Racine des fichiers téléversés servie en statique (plans, logos, sons) |
Architecture
Découpage en quatre couches, la dépendance allant toujours vers l'intérieur : Domain → Application → Infrastructure → Presentation. Namespaces en FireSupervisor.Backend.<Couche>.<Module>. Les contrôleurs ne portent pas de logique métier.
Le chemin d'une lecture Modbus jusqu'à l'écran
| Étape | Acteur | Ce qui s'y passe |
|---|---|---|
| 1 | SupervisionWorker | Une tâche par centrale ; recharge la configuration de la centrale à chaque itération, si bien qu'un changement d'adresse ou de profil est pris au cycle suivant |
| 2 | PanelAdapterFactory | Choisit l'adaptateur et la politique de timing d'après le PanelType, dimensionne la fenêtre de lecture |
| 3 | Adaptateur | Lit les régions déclarées par le profil et rend un PanelSnapshot |
| 4 | FlagStabilization | Filtre les transitoires avant la machine à états |
| 5 | PanelStateService | Applique l'écart, journalise, décide de ce qui a changé |
| 6 | PanelStateManager | Détient l'état en mémoire, un sémaphore par centrale |
| 7 | SignalRPanelStateNotifier | Pousse le PanelDto sur ModbusHub |
Les commandes suivent le chemin inverse : PanelsController → service métier → adaptateur → écriture Modbus.
Mécanismes non évidents
L'état vit en mémoire, la base n'en est que la trace
PanelStateManager est un singleton qui détient les FirePanel dans un ConcurrentDictionary, avec un SemaphoreSlim par centrale. La base porte la configuration et l'historique, pas l'état courant.
| Conséquence | Détail |
|---|---|
| Toute écriture de configuration doit toucher la copie mémoire | Une centrale modifiée en base sans mise à jour de la copie mémoire continue d'être interrogée avec l'ancienne configuration |
| L'état est reconstruit au démarrage | PanelStateLoader charge les centrales depuis la base avant que la supervision ne démarre |
Les libellés d'événement viennent des ZoneConfigs | Ils passent devant Zone.Label dans les journaux ; tout chemin qui modifie une centrale appelle SetZoneConfigs sur la copie mémoire |
Un état ne change qu'après confirmation, sauf s'il est dangereux
FlagStabilization est le point de passage unique appliqué à l'ensemble des drapeaux lus, avant la machine à états par drapeau.
| Transition | Traitement |
|---|---|
Apparition d'un drapeau de danger — Fire, Fault, Emission, Evacuation, DMExtinction | Appliquée immédiatement, jamais retardée |
| Apparition d'un drapeau de maintenance — hors service, essai | Confirmée sur StateConfirmationCycles cycles consécutifs |
| Disparition d'un drapeau, quel qu'il soit | Confirmée sur StateConfirmationCycles cycles consécutifs |
La direction est toujours sûre : un incident reste affiché plus longtemps, jamais moins. La stabilisation est exclusivement backend — aucune couche cliente n'en refait le travail.
Une centrale au firmware plus ancien reste connectée
GlobalCoilReader lit la table des paramètres généraux dans sa taille étendue. Une centrale qui ne sert pas les bobines d'extension répond par une exception esclave — une réponse protocolaire, pas une panne de transport.
| Comportement | Détail |
|---|---|
| Lecture optimiste | Un firmware conforme n'émet qu'une trame par cycle, comme s'il n'y avait pas de repli |
| Dégradation mémorisée | Une centrale dégradée émet deux trames au premier cycle, puis une seule |
| Re-sondage | L'instance est reconstruite avec l'adaptateur à chaque reconnexion ; ProbeInterval couvre une centrale qui ne se déconnecte jamais |
| Verdict | La centrale reste connectée ; seules les synthèses d'exploitation deviennent indisponibles |
Une coupure après commande n'est pas un dérangement
SupervisionWorker retient l'instant de la dernière écriture par centrale et applique une fenêtre de grâce de quinze secondes (CommandGrace). L'INIS-E16 n'accuse pas réception de l'écriture et ferme sa connexion dans la foulée ; le lien revient de lui-même une douzaine de secondes plus tard. Sans cette fenêtre, chaque commande inscrirait au journal réglementaire un dérangement de communication qui n'en est pas un. La fenêtre suspend le verdict, jamais les tentatives de reconnexion.
Le hub SignalR est partagé, le rôle ne se contrôle que sur un point
ModbusHub porte [Authorize] au niveau de la classe : tout utilisateur authentifié reçoit l'état des centrales. Le contrôle de rôle vit uniquement dans JoinConsole, qui refuse l'abonnement au groupe SuperAdminConsole — celui qui reçoit les trames Modbus brutes — à qui n'est pas SuperAdmin.
Une action qui modifie des données partagées doit se déclarer
| Élément | Rôle |
|---|---|
NotifiesResourceAttribute | Posé sur un contrôleur ou une action, déclare la famille de données touchée |
ResourceChangeFilter | Émet la notification après l'action, sur POST/PUT/PATCH/DELETE et seulement si la réponse est un succès |
ResourceChangeCoverageTests | Parcourt les contrôleurs par réflexion et exige la déclaration sur toute action modifiante |
Le compilateur ne vérifie rien : une action qui oublie l'attribut laisse les autres postes sur des données périmées, sans erreur. C'est le test réflexif qui l'attrape — et il ne voit pas le même jeu de types en Debug et en Release.
Le secret applicatif est unique et sans valeur par défaut
SecurityDefaults.Resolve résout Security:SuperAdminPassword et lève une exception fatale s'il est absent : aucun repli codé en dur. Ce secret sert à la fois :
| Usage | Où |
|---|---|
| Mot de passe du compte SuperAdmin, haché BCrypt au démarrage | DbInitializer |
| Mot de passe du rôle PostgreSQL applicatif, injecté dans la chaîne de connexion | StartupExtensions.BuildConnectionString |
| Mot de passe du superutilisateur du cluster embarqué | Posé par l'installateur |
Si l'authentification du rôle applicatif échoue (28P01), le démarrage réaligne le mot de passe du rôle en passant par le superutilisateur — qui porte le même secret. Aucun mot de passe de superutilisateur n'est donc figé dans le binaire livré.
Le fichier de secrets se scelle tout seul au premier démarrage
CryptoConfigProtector choisit sa source selon l'environnement, et modifie le disque en production.
| Environnement | Fichier lu | Effet sur le disque |
|---|---|---|
| Développement, tests, hors Windows | appsettings.Crypto.dev.json | Aucun : le fichier reste en clair et éditable |
| Production Windows, premier démarrage | appsettings.Crypto.prod.json | Chiffré DPAPI en appsettings.Crypto.prod.protected, puis le fichier clair est supprimé |
| Production Windows, démarrages suivants | appsettings.Crypto.prod.protected | Déchiffré en mémoire |
| Repli, postes antérieurs | appsettings.Crypto.json / appsettings.Crypto.protected | Le blob hérité est migré vers le nouveau nom |
Le blob DPAPI est de portée machine : copié ailleurs, il est inexploitable.
Le poste de développement monte son propre PostgreSQL
DevPostgresCluster crée un cluster dans le profil de l'utilisateur au premier dotnet run. Rien n'est installé ni enregistré comme service.
| Binaires cherchés dans | SUPERVISEUR_PG_BIN s'il est défini, sinon C:\Program Files\PostgreSQL\17\bin puis …\16\bin |
| Cluster | %LOCALAPPDATA%\Superviseur_Dev\pgdata |
| Port | 5433 — un service installé occupe le 5432 |
| Secret du superutilisateur | Celui de appsettings.Development.json, versionné et sans valeur ailleurs |
Rôle applicatif et base sont ensuite créés par les mêmes chemins qu'en production.
La clé de signature JWT est persistée hors du dépôt
JwtKeyProvider résout la clé dans cet ordre : la valeur configurée (JwtSecret de app.config.json, tenue pour non configurée si elle commence par FireSupervisor_Secret_Key), puis le fichier jwt.key du répertoire de données applicatif, puis une clé de 256 bits générée et persistée. Sans cette persistance, un redémarrage du service invaliderait toutes les sessions.
Le certificat TLS a un repli silencieux
StartupExtensions.ResolveServerCertificate charge le .pfx désigné par la section Https. En son absence, il génère en mémoire un certificat auto-signé CN=localhost valable cinq ans. Ce certificat de repli n'est approuvé par aucun client : un poste qui n'a pas reçu son certificat à l'installation démarre quand même, et l'anomalie ne se voit que côté navigateur.
Services hébergés
| Service | Rythme | Effet |
|---|---|---|
PanelStateLoader | Au démarrage | Charge les centrales de la base vers la mémoire |
SupervisionWorker | Boucle de gestion toutes les 5 s, plus une tâche par centrale | Interroge les centrales, relance une tâche terminée prématurément |
ModbusMetricsFlushService | MetricsFlushIntervalMinutes | Persiste un instantané agrégé par centrale et purge au-delà de MetricsRetentionDays |
AuditLifecycleService | Démarrage et arrêt | Consigne le cycle de vie du service au journal réglementaire |
AuditTrailRetentionService | Quotidien | Purge au-delà de la durée légale, avec un plancher de cinq ans garanti quelle que soit la configuration ; suspend la purge si l'espace disque devient critique |
ReportSchedulerService | Planifié | Génère les rapports automatiques et purge les archives |
Le journal d'audit n'échoue jamais l'action métier
AuditTrailService enrichit chaque entrée de l'acteur courant — nom, rôle, adresse IP — et absorbe toute défaillance de persistance. Une action réussie dont l'audit n'a pas pu s'écrire reste donc invisible au journal : l'action de supervision prime sur sa trace.
Configuration
Réglages à effet observable, dans appsettings.json. Toute clé est surchargeable par variable d'environnement (SupervisionSettings__PollingIntervalSeconds, …).
| Clé | Défaut | Effet |
|---|---|---|
SupervisionSettings:PollingIntervalSeconds | 2 | Intervalle de cycle, surchargé par pollingInterval (ms) de la configuration globale en base |
SupervisionSettings:MinPollingIntervalMs | 200 | Plancher appliqué quel que soit le réglage utilisateur |
SupervisionSettings:StateConfirmationCycles | 2 | Cycles exigés pour confirmer une transition non dangereuse |
SupervisionSettings:ConnectionTimeoutSeconds | 5 | Surchargé par connectionTimeout en base ; plancher de 1 s |
SupervisionSettings:InterRequestDelayMs | 75 | Silence entre deux requêtes ; l'AVISS impose un plancher de 50 ms |
SupervisionSettings:MaxReconnectBackoffMs | 10000 | Recul maximal après échecs consécutifs |
SupervisionSettings:MetricsRetentionDays | 90 | Rétention des mesures de liaison |
Jwt:ExpiryMinutes | 480 | Durée de vie du jeton d'accès |
Auth:RefreshTokenDays | 30 | Durée de vie du jeton de rafraîchissement |
Auth:PasswordPolicy | 12 caractères, majuscule, minuscule, chiffre | Politique appliquée à la création et au changement de mot de passe |
Auth:LoginCooldown | 5 tentatives / 300 s, blocage 60 s | Anti-force brute, doublé d'une limitation de débit par IP sur les points d'authentification |
LogRetentionYears | 5 | Rétention du journal réglementaire, avec un plancher de cinq ans |
Https:CertificatePath / Https:CertificatePassword | vides | Certificat TLS ; à défaut, auto-signé en mémoire |
AllowedHosts | * | Ouvert, et tracé comme tel au dossier CRA |
DemoSeed:Enabled | false | Jeu de démonstration issu de demo-fixtures.json |
Database:StartupConnectRetries | 10 | Tentatives de connexion à la base au démarrage, espacées de Database:StartupConnectRetryDelaySeconds (3) |
Ordre de chargement : appsettings.json, puis appsettings.<Environnement>.json, puis la source crypto résolue par CryptoConfigProtector. Le dernier chargé l'emporte.
La politique CORS n'accepte que localhost, l'hôte virtuel du client de bureau et les plages IP privées RFC 1918 ; aucune origine publique.
Contraintes actives
| Contrainte | Conséquence si elle est ignorée |
|---|---|
| Le secret applicatif n'a aucune valeur par défaut | Le service refuse de démarrer, par choix : un défaut codé en dur serait public |
Ne jamais modifier ni afficher le contenu des appsettings.Crypto.*.json | Ils portent les clés revendeurs et le secret applicatif |
appsettings.Crypto.prod.json doit exister sur la machine de build avant un publish en Release | La compilation aboutit, mais le service publié refuse de démarrer |
appsettings.Crypto.dev.json n'entre jamais dans l'installateur | Il porte un mot de passe en clair ; le .csproj ne l'inclut qu'en Debug |
La configuration Release compile un autre jeu de types que Debug | DemoSeedController, DemoDataSeeder et des actions de MapsController / AppConfigsController sont sous #if DEBUG ; les tests réflexifs voient donc autre chose que la CI |
Toute action modifiante de données partagées porte [NotifiesResource] | Les postes connectés restent sur des données périmées, sans erreur |
| Toute entité ajoutée est sauvegardée ou écartée avec son motif | BackupCoverageTests échoue ; sinon la perte n'apparaîtrait qu'à la restauration |
| Un profil livré non modifié est réaligné sur le code à la restauration | Sans quoi une archive figerait des adresses corrigées depuis |
InternalsVisibleTo n'est ouvert qu'à backend.tests | Aucun autre projet ne voit les types internes |
Hors Debug, publier le backend lance npm install et npm run build | Node.js est requis sur la machine de build ; -p:SkipFrontend=true désactive cette cible |
Tests
Les tests vivent dans dotnet/tests/backend — voir son README.
powershell
dotnet test dotnet/tests/backend/backend.tests.csproj -c Release