Thème
Frontend — interface de supervision
Application monopage Vue 3 servant l'ensemble de l'interface : tableau de bord, carte du site, détail des centrales, historique, statistiques, rapports, réglages et outils de diagnostic. Elle consomme l'API REST du backend et reçoit l'état des centrales en temps réel par SignalR.
| Technologie | Vue 3 (Composition API, <script setup>), TypeScript, Vite 6, Tailwind CSS 4 + DaisyUI 5, Pinia, Vue Router, vue-i18n |
| Point d'entrée | src/main.ts puis src/App.vue |
| Temps réel | @microsoft/signalr sur le hub /modbusHub |
| HTTP | Axios, encapsulé par src/api/httpClient.ts |
| Node requis | `^20.19.0 |
| Sortie de build | dist/, recopiée dans le wwwroot du backend et dans celui du client de bureau |
| Langues | Français et anglais, src/i18n/locales/ |
Commandes
bash
npm install
npm run dev # serveur de développement sur :5173, proxy vers le backend :5264
npm run build # vérification de types puis bundle dans dist/
npm run test:run # Vitest
npm run test:e2e # Playwright
npm run lint # eslint --fix
npm run lint:ci # eslint --max-warnings 0, sans correction
npm run docs # TypeDoc vers docs/site/api/Le serveur de développement proxifie /api, /modbusHub (WebSocket comprise), /uploads et /modbus-scanner.html vers http://127.0.0.1:5264.
Structure
| Dossier | Contenu |
|---|---|
src/features/ | Un dossier par domaine : auth, dashboard, help, history, inis-network, map, notes, panel, reports, settings, statistics, status, tools. Chacun porte sa vue, ses composables use*, ses sous-composants, son store et ses types |
src/api/ | Accès HTTP. apiService.ts agrège ; les méthodes sont réparties par domaine dans services/ (auth, users, logs, panels, inis, modbus-profiles, reports, notes, system), les types dans apiTypes.ts, le client SignalR dans signalr.service.ts |
src/components/ui/ | Composants partagés : DataTable, BaseModal, SearchableSelect, BaseIcon, ConfirmDialog, FilterBar, NotificationStack… |
src/composables/ | Composables transverses : points de rupture, navigation, notifications, raccourcis clavier, échelle automatique, habillage des incidents |
src/stores/ | Stores Pinia globaux : configuration, connexion, profils d'incident, console Modbus, synchronisation de ressources, barre latérale, modifications non enregistrées |
src/services/ | Services applicatifs non HTTP : confirmation, notifications, et deux services audio distincts |
src/layouts/ | MainLayout.vue et son chrome (barre latérale, en-tête, pied, navigation basse) |
src/router/ | 16 routes et garde de navigation (requiresAuth, requiresSuperAdmin) |
src/config/ | Constantes, échelle de sévérité, recettes de sons d'interface, et generated/enums.ts |
src/i18n/ | Initialisation et dictionnaires fr.json / en.json |
src/types/, src/utils/ | Types transverses et utilitaires (décodeur Modbus, greffon de zoom des graphiques) |
src/test/, src/test-helpers/ | Amorce Vitest et constructeurs de scénarios Modbus |
e2e/ | Suite Playwright : specs/, pages/, support/ |
137 fichiers de spécification Vitest sont co-localisés avec les sources (*.spec.ts ou *.test.ts à côté du composant éprouvé) et s'exécutent sous jsdom.
Architecture
Organisation par feature : chaque domaine fonctionnel regroupe sa vue, sa logique et ses sous-composants. La logique des gros composants est isolée dans des composables use* co-localisés — usePanelConfigForm, useStatistics, useDashboard, ou les composables de carte dans features/map/composables/. Le composant garde un <script setup> mince : defineProps/Emits/Expose, imports de sous-composants et le câblage const { … } = useXxx(props, emit) ; le gabarit ne dépend que des valeurs rendues par le composable.
Navigation : MainLayout héberge la vue routée dans un <router-view> enveloppé d'une <transition> et d'un <keep-alive>, la clé étant $route.path.
Mécanismes non évidents
L'URL du backend n'est jamais relative
src/api/apiBase.ts est la source unique. Elle résout l'URL de base dans cet ordre :
| Rang | Source | Cas |
|---|---|---|
| 1 | VITE_API_BASE_URL | Défini à la compilation ; en développement, pointe le backend :5264 |
| 2 | window.__SUPERVISEUR_API_BASE__ | Injecté par le client de bureau, qui charge le front depuis le disque et indique où joindre le service |
| 3 | location.origin | Le front est servi par le backend lui-même |
assetUrl() applique la même base à tout téléversement affiché — logo, sons, plans. Une URL /api ou /uploads écrite en relatif fonctionne en navigateur et échoue silencieusement sur poste installé, où le front est servi par un hôte virtuel distinct du backend. La spécification relative-api-calls.spec.ts verrouille cette règle.
Corollaire : aucune ressource ne vient d'un CDN. Toute dépendance chargée à l'exécution passe par un import ?url.
Les énumérations partagées sont générées, pas écrites
src/config/generated/enums.ts est produit par TsEnumGenerator depuis les enums C# du backend. Le fichier est versionné, et l'intégration continue échoue s'il diffère de ce que le générateur produirait. La valeur de chaque membre est son nom, forme sérialisée par le backend.
L'application entière est mise à l'échelle par zoom sur <body>
useAutoZoom pose une propriété CSS zoom en style en ligne sur <body>.
| Palier | Échelle |
|---|---|
| Bureau | Proportionnelle à la fenêtre, référence 1680 × 945, plafonnée à 0,9 et plancher à 0,5 |
| Tablette | 0,9 fixe |
| Mobile | 0,85 fixe |
Routes /notice et /login | Taille réelle |
Aucun écran n'est donc à l'échelle 1. Tout code qui mêle des coordonnées de pointeur — clientX, getBoundingClientRect, en pixels visuels, échelle comprise — à des longueurs CSS — interprétées avant mise à l'échelle — doit diviser par bodyScale(), sans quoi la géométrie dérive de tout l'écart d'échelle.
Deux services audio, deux natures
| Service | Nature | Source |
|---|---|---|
sound.service.ts | Sons d'alarme | Fichiers de public/sounds/, remplaçables par site |
ui-sound.service.ts | Retours d'interface | Synthétisés en WebAudio d'après les recettes de config/ui-sounds.ts — aucun fichier audio |
Un geste de l'opérateur se réaligne sur le serveur
Le backend ne notifie que sur changement : un poste qui manquerait la notification suivant son propre geste resterait indéfiniment sur une estimation locale. refreshPanelFromServer (panel.store.ts) relit donc la centrale par GET /api/Panels/{id} et applique la réponse par le chemin d'une mise à jour temps réel, dont elle partage source et projection. Un échec est sans conséquence visible : le prochain cycle de supervision rétablira la vérité dès que la centrale bougera.
Le keep-alive du layout ne déclare ni include ni exclude
Les vues sont conservées telles quelles ; le rafraîchissement d'un écran réactivé passe par onActivated — useDashboard, useMapEditor, UnifiedMap — et par des stores idempotents.
Tests de bout en bout
La suite pilote un vrai navigateur contre le backend réel. Trois serveurs sont démarrés automatiquement, ou réutilisés s'ils tournent déjà : le simulateur sans interface — en tête, pour que le backend trouve ses centrales dès sa première tentative —, le backend :5264 avec le jeu de démonstration, et Vite :5173. Prérequis identiques au développement : PostgreSQL local et dotnet/backend/appsettings.Crypto.dev.json.
bash
npm run test:e2e # sans interface
npm run test:e2e:ui # interface interactiveDeux centrales de démonstration sont réellement servies en Modbus — une INIS-E16 en TCP, une AVISS en RTU sur tube nommé — ce qui permet d'exercer la chaîne complète : état « connecté », alarme posée sur la centrale et remontée à l'écran, commande d'exploitation qui atteint la centrale. Les autres centrales [DEMO] restent déconnectées, et leurs incidents ne sont pas acquittables.
| Dossier | Contenu |
|---|---|
e2e/specs/ | 15 fichiers de scénarios : auth, roles, navigation, status, history, statistics, reports, users, settings, notes, supervision, incidents, custom-profile, panel-profiles, sound-upload |
e2e/pages/ | Page objects : chaque écran expose ses locators (data-testid) et ses actions métier ; les specs ne manipulent jamais de sélecteur directement. select.control.ts pilote n'importe quel SearchableSelect, dont le menu est téléporté dans body |
e2e/support/ | fixtures.ts (test de base, forPage pour une seconde session simultanée), accounts.ts, auth.setup.ts, simulator.ts, api.ts |
Comptes du jeu de démonstration : demo.admin01, demo.user01, demo.super01, mot de passe Demo1234!. auth.setup.ts ouvre chaque session une fois par exécution via l'API et la sauvegarde en storageState — les points d'authentification sont limités à 10 requêtes par minute.
Rendre la flotte au parcours suivant
Un état qui retombe sans acquittement reste exposé à l'opérateur : remettre les bits à zéro côté simulateur ne suffit pas à nettoyer. settlePanel attend le délai de confirmation du backend, réinitialise les zones et les points, puis acquitte les synthèses rétablies — que la réinitialisation globale ne couvre pas. resetSimulatedFleet fait de même avant une série, car une exécution précédente ou interrompue laisse sinon des états qu'un parcours exigeant une centrale nominale ne pardonne pas.
Accroches génériques
BaseModal (prop dataTestid et bouton modal-close), DataTable (table-row), FilterBar (filter-search), ConfirmDialog (confirm-accept / confirm-cancel).
Ce que la suite n'affirme pas, faute d'historique semé : les volumes d'événements. Les écrans concernés sont vérifiés sur leurs mécaniques — filtres, bascules, liens directs — et sur le CRUD réel. Le décodage des adresses Modbus et les règles de groupement des incidents relèvent des tests d'intégration backend et des spécifications Vitest, plus rapides et plus précis.
Contraintes actives
| Contrainte | Conséquence si elle est ignorée |
|---|---|
Aucune URL /api ou /uploads relative | Fonctionne en navigateur, échoue sans message sur poste installé |
Aucun CDN ; toute dépendance d'exécution en import ?url | Même symptôme, sur un poste hors ligne |
src/config/generated/enums.ts ne s'édite pas à la main | L'édition est perdue, et l'intégration continue la signale |
lint:ci et non lint en intégration continue | lint corrige (--fix) et ferait passer au vert du code non conforme, le correctif étant jeté avec l'agent |
Aucun zoom ni transform sur <html> | zoom sur <html> casse le caret des zones de texte ; transform produit un débordement défilable |
Les surcouches placées sous un backdrop-blur passent par Teleport vers body | Elles seraient rendues derrière le flou |
| Un gabarit de vue routée n'a qu'un seul nœud racine | Un commentaire HTML en compte comme un : la <transition> du layout devient inopérante et la page reste vide, sans erreur |