Skip to content

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.

TechnologieC# .NET 9, ASP.NET Core, EF Core 9 + Npgsql, SignalR, Serilog, NModbus
Point d'entréeProgram.cs — assemblage délégué à StartupExtensions.cs
Nom d'assemblySuperviseur-Service
Service WindowsSuperviseur-Service (ServiceName de app.config.json)
Écoutehttps://localhost:5000 en production, http://localhost:5264 en développement
Référence projetdotnet/shared
Consommateursvue, dotnet/client, dotnet/manager, dotnet/tools/TsEnumGenerator

Structure ​

DossierContenu
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 ​

ÉtapeActeurCe qui s'y passe
1SupervisionWorkerUne 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
2PanelAdapterFactoryChoisit l'adaptateur et la politique de timing d'après le PanelType, dimensionne la fenêtre de lecture
3AdaptateurLit les régions déclarées par le profil et rend un PanelSnapshot
4FlagStabilizationFiltre les transitoires avant la machine à états
5PanelStateServiceApplique l'écart, journalise, décide de ce qui a changé
6PanelStateManagerDétient l'état en mémoire, un sémaphore par centrale
7SignalRPanelStateNotifierPousse 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équenceDétail
Toute écriture de configuration doit toucher la copie mémoireUne 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émarragePanelStateLoader charge les centrales depuis la base avant que la supervision ne démarre
Les libellés d'événement viennent des ZoneConfigsIls 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.

TransitionTraitement
Apparition d'un drapeau de danger — Fire, Fault, Emission, Evacuation, DMExtinctionAppliquée immédiatement, jamais retardée
Apparition d'un drapeau de maintenance — hors service, essaiConfirmée sur StateConfirmationCycles cycles consécutifs
Disparition d'un drapeau, quel qu'il soitConfirmé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.

ComportementDétail
Lecture optimisteUn firmware conforme n'émet qu'une trame par cycle, comme s'il n'y avait pas de repli
Dégradation mémoriséeUne centrale dégradée émet deux trames au premier cycle, puis une seule
Re-sondageL'instance est reconstruite avec l'adaptateur à chaque reconnexion ; ProbeInterval couvre une centrale qui ne se déconnecte jamais
VerdictLa 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émentRôle
NotifiesResourceAttributePosé 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
ResourceChangeCoverageTestsParcourt 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 :

UsageOù
Mot de passe du compte SuperAdmin, haché BCrypt au démarrageDbInitializer
Mot de passe du rôle PostgreSQL applicatif, injecté dans la chaîne de connexionStartupExtensions.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.

EnvironnementFichier luEffet sur le disque
Développement, tests, hors Windowsappsettings.Crypto.dev.jsonAucun : le fichier reste en clair et éditable
Production Windows, premier démarrageappsettings.Crypto.prod.jsonChiffré DPAPI en appsettings.Crypto.prod.protected, puis le fichier clair est supprimé
Production Windows, démarrages suivantsappsettings.Crypto.prod.protectedDéchiffré en mémoire
Repli, postes antérieursappsettings.Crypto.json / appsettings.Crypto.protectedLe 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 dansSUPERVISEUR_PG_BIN s'il est défini, sinon C:\Program Files\PostgreSQL\17\bin puis …\16\bin
Cluster%LOCALAPPDATA%\Superviseur_Dev\pgdata
Port5433 — un service installé occupe le 5432
Secret du superutilisateurCelui 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 ​

ServiceRythmeEffet
PanelStateLoaderAu démarrageCharge les centrales de la base vers la mémoire
SupervisionWorkerBoucle de gestion toutes les 5 s, plus une tâche par centraleInterroge les centrales, relance une tâche terminée prématurément
ModbusMetricsFlushServiceMetricsFlushIntervalMinutesPersiste un instantané agrégé par centrale et purge au-delà de MetricsRetentionDays
AuditLifecycleServiceDémarrage et arrêtConsigne le cycle de vie du service au journal réglementaire
AuditTrailRetentionServiceQuotidienPurge 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
ReportSchedulerServicePlanifié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éfautEffet
SupervisionSettings:PollingIntervalSeconds2Intervalle de cycle, surchargé par pollingInterval (ms) de la configuration globale en base
SupervisionSettings:MinPollingIntervalMs200Plancher appliqué quel que soit le réglage utilisateur
SupervisionSettings:StateConfirmationCycles2Cycles exigés pour confirmer une transition non dangereuse
SupervisionSettings:ConnectionTimeoutSeconds5Surchargé par connectionTimeout en base ; plancher de 1 s
SupervisionSettings:InterRequestDelayMs75Silence entre deux requêtes ; l'AVISS impose un plancher de 50 ms
SupervisionSettings:MaxReconnectBackoffMs10000Recul maximal après échecs consécutifs
SupervisionSettings:MetricsRetentionDays90Rétention des mesures de liaison
Jwt:ExpiryMinutes480Durée de vie du jeton d'accès
Auth:RefreshTokenDays30Durée de vie du jeton de rafraîchissement
Auth:PasswordPolicy12 caractères, majuscule, minuscule, chiffrePolitique appliquée à la création et au changement de mot de passe
Auth:LoginCooldown5 tentatives / 300 s, blocage 60 sAnti-force brute, doublé d'une limitation de débit par IP sur les points d'authentification
LogRetentionYears5Rétention du journal réglementaire, avec un plancher de cinq ans
Https:CertificatePath / Https:CertificatePasswordvidesCertificat TLS ; à défaut, auto-signé en mémoire
AllowedHosts*Ouvert, et tracé comme tel au dossier CRA
DemoSeed:EnabledfalseJeu de démonstration issu de demo-fixtures.json
Database:StartupConnectRetries10Tentatives 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 ​

ContrainteConséquence si elle est ignorée
Le secret applicatif n'a aucune valeur par défautLe 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.*.jsonIls portent les clés revendeurs et le secret applicatif
appsettings.Crypto.prod.json doit exister sur la machine de build avant un publish en ReleaseLa compilation aboutit, mais le service publié refuse de démarrer
appsettings.Crypto.dev.json n'entre jamais dans l'installateurIl 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 DebugDemoSeedController, 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 motifBackupCoverageTests échoue ; sinon la perte n'apparaîtrait qu'à la restauration
Un profil livré non modifié est réaligné sur le code à la restaurationSans quoi une archive figerait des adresses corrigées depuis
InternalsVisibleTo n'est ouvert qu'à backend.testsAucun autre projet ne voit les types internes
Hors Debug, publier le backend lance npm install et npm run buildNode.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

Documentation Technique