LabTracker¶
Tableau de bord de suivi des échantillons pour le contrôle qualité des matériaux de construction de LCL Chantiers (béton, granulats, enrobés). Il synchronise les soumissions terrain depuis ODK Central, suit chaque éprouvette à travers le laboratoire, saisit et calcule les résultats d'essais, produit les rapports de certification en PDF, et les transmet au portail central de rapports de l'entreprise.
Pour commencer¶
Le problème qu'il résout¶
Un technicien de chantier prélève un échantillon sur le terrain et remplit un formulaire dans ODK Collect (Android). La soumission arrive dans ODK Central. Du côté du laboratoire, il faut savoir quels échantillons sont arrivés, lesquels sont en attente, lesquels sont en retard, qui en est responsable, quels sont les résultats d'essais, et s'ils respectent le devis, avec une trace de tout ce qui a changé.
Le concept à comprendre en premier¶
L'unité de suivi n'est pas l'échantillon, c'est l'éprouvette.
Un échantillon est le matériau physique prélevé sur le chantier. Il est divisé en une ou plusieurs éprouvettes, et c'est l'éprouvette qui porte un statut, une priorité, une échéance, un laboratoire assigné et ses propres essais.
- Béton : un échantillon → plusieurs cylindres, affichés
-01,-02,-03. - Granulats / enrobé : un échantillon → exactement une éprouvette, sans suffixe.
Presque tous les écrans et tableaux découlent de cette distinction. OdkSample conserve ce que le terrain a rapporté ; SampleSpecimen conserve ce que fait le laboratoire.
Lancer en local¶
cp .env.example .env # DB password + ODK credentials
docker compose up -d db # Postgres, mapped to 5433 to avoid conflicts
# Create src/LabTracker.Web/appsettings.Development.json — see Configuration below.
# It is git-ignored and holds real secrets.
cd src/LabTracker.Web
dotnet run # migrations are applied automatically on startup
Application sur http://localhost:5000. La connexion passe par Azure AD, un compte dans le tenant est donc nécessaire.
dotnet test # the calculation engine's unit tests
Architecture¶
Deux projets plus un projet de tests :
| Projet | Contient |
|---|---|
| LabTracker.Core | Entités du domaine, EF Core, client ODK et worker de synchronisation, règles d'échéance, moteur de calcul des résultats d'essais, générateurs de rapports PDF, envoi au portail |
| LabTracker.Web | Interface Blazor Server et les workers en arrière-plan, dans un seul processus |
| LabTracker.Core.Tests | Tests xUnit du moteur de calcul et de ses règles |
PostgreSQL 16 conserve les payloads bruts d'ODK dans des colonnes jsonb, à côté de colonnes typées qui en sont extraites, de sorte qu'un champ jamais promu en colonne reste tout de même récupérable.
Deux workers BackgroundService tournent dans le processus web :
SyncWorker: récupère les jeux de données ODK à intervalle régulier.ReportDeliveryWorker: transmet les rapports PDF en file d'attente au portail de rapports, avec réessais.
Flux de données¶
ODK Central ──sync──► OdkSample ──► SampleSpecimen ──► SpecimenTest ──► TestResult
│ │
status, deadline, computed values
lab, priority + conformity
│
PDF report
│
report portal (HTTP)
Stack technique¶
- .NET 10 · Blazor Server (rendu interactif côté serveur)
- EF Core 10 + Npgsql · PostgreSQL 16
- Microsoft.Identity.Web : authentification Azure AD (OpenID Connect)
- QuestPDF : génération des rapports
- Docker Compose, déployé via Coolify
Tout exige un utilisateur authentifié : une politique d'autorisation par défaut couvre chaque endpoint. Le claim name de l'utilisateur connecté est inscrit sur chaque entrée d'historique.
Synchronisation ODK¶
SyncWorker effectue un premier passage 15 secondes après le démarrage, puis toutes les N minutes (10 par défaut). Le bouton ↻ du tableau de bord déclenche un cycle à la demande.
| Jeu de données | Mapper | Préfixe de champ |
|---|---|---|
| projets | ProjectMapper | - |
| specs_beton | BetonSpecMapper | sb_* |
| specs_enrobe | EnrobeSpecMapper | - |
| specs_granulaires | GranulairesSpecMapper | sg_* |
| echantillons_beton | BetonSampleMapper | eb_* |
| echantillons_granulaires | GranulairesSampleMapper | eg_* |
| echantillons_enrobe | EnrobeSampleMapper | ee_* |
| essais_beton | BetonEssaiMapper | - |
Chaque jeu de données est acheminé vers l'IEntityMapper enregistré pour son EntityKind.
À savoir avant de toucher à la synchronisation :
- Curseur incrémental. Seules les entités modifiées depuis
LastSyncedAtsont récupérées. Le curseur est capturé avant le travail et avancé seulement après une récupération complètement réussie, de sorte qu'un échec en cours de cycle est réessayé plutôt qu'ignoré. - Date de coupure.
OdkOptions.CutoffUtcfait ignorer par la synchronisation tout échantillon créé avant cette date, ce qui écarte le bruit historique. - L'ordre des jeux de données n'est pas garanti.
DatasetConfigs.Kindest persisté comme une string, donc l'ordre du worker est alphabétique. Ne jamais présumer qu'un jeu de données est synchronisé avant un autre (voir la section sur le béton frais ci-dessous pour la façon dont c'est géré). - Les échecs sont contenus. Une erreur sur un jeu de données est journalisée puis absorbée, pour que la boucle continue de tourner.
Essais de béton frais sur chantier¶
Le jeu de données essais_beton porte l'affaissement, la teneur en air et la température mesurés sur le chantier pendant que le béton est encore frais. Chaque entité est stockée dans sa propre table BetonEssais ; après chaque cycle de synchronisation, FreshConcreteApplier copie les valeurs sur l'OdkSample correspondant, apparié par numéro de projet + numéro d'échantillon.
Cette conception en deux étapes est délibérée : un essai peut être récupéré avant l'échantillon auquel il appartient, donc l'appliquer directement dans le mapper le perdrait à mesure que le curseur avance. Il reste plutôt en attente et est appliqué à un cycle ultérieur. Comme les valeurs vivent sur l'échantillon, chaque éprouvette de cet échantillon les reçoit d'un coup.
Le déroulement au laboratoire¶
Échéances¶
Une stratégie par matériau (Core/Lab/), distribuée par DeadlineService :
- Béton : premier cylindre à prélèvement + 7 jours, les autres à + 28 jours.
- Granulaires / Enrobé : délais provisoires ; les véritables règles de délai attendues du client restent à confirmer.
Calculée à la création de l'éprouvette, et recalculée seulement si la date de prélèvement change. Une modification manuelle active DeadlineOverridden, qui la protège définitivement de tout recalcul.
Essais et charge de travail¶
Chaque éprouvette porte un ou plusieurs SpecimenTest. TestCatalog conserve les essais prédéfinis et leurs durées standards :
| Essai | Matériau | Heures standards |
|---|---|---|
| Bris de cylindre 7 j | Béton | 0.25 |
| Bris de cylindre 28 j | Béton | 0.25 |
| Granulométrie | Granulaires | 1.25 |
| Proctor | Granulaires | 2.5 |
| Autre (texte libre) | - | porte son propre nom et ses propres heures |
WorkloadService additionne les heures en attente par laboratoire. Les laboratoires actifs sont Granby et Sherbrooke.
Résultats d'essais : le moteur de calcul¶
À manipuler avec précaution
LabTracker recalcule les essais ; il ne se contente pas de stocker des valeurs finales. Le moteur vit dans Core/Lab/Results/ sous forme de fonctions pures, sans effet de bord, ce qui le rend testable unitairement. C'est aussi la partie à traiter avec le plus de précaution, puisqu'un chiffre erroné ici devient silencieusement un résultat de certification.
| Fichier | Responsabilité |
|---|---|
GranulometrieCalculator |
Masses des tamis → courbe de passants combinée, pertes granulo, D10/D30/D50/D60, Cu/Cc, module de finesse, fractions du sol |
ProctorCalculator |
Points de compactage → teneur en eau et densités, sommet de la courbe ajustée, sélection de la méthode A/B/C, correction pour gros granulats |
BetonResistanceCalculator |
kN → MPa avec une table de correction d'élancement |
GradingEnvelopeCatalog |
Les 14 fuseaux granulométriques MTQ/BNQ fixes (MG-20, CG-14, Sable, Nette…) et la conformité par tamis |
ConformityService |
Verdicts contre le devis : résistance du béton selon l'âge, courbe granulométrique contre son fuseau, seuil de perte granulo |
SpecimenStatusRules |
Dérive le statut de l'éprouvette à partir des résultats de ses essais |
Le béton fait exception : la presse rapporte déjà des MPa, donc la résistance est saisie directement. BetonResistanceCalculator n'est donc pas utilisé par l'interface ; il est conservé, avec ses tests, au cas où un flux basé sur la charge reviendrait.
Règles de conformité qui comptent :
- Béton : un bris doit atteindre 70 % de la résistance spécifiée à 7 jours, 100 % à 28 jours.
- Granulométrie : la courbe doit rester à l'intérieur de son fuseau, jugée uniquement sur les tamis que le fuseau liste explicitement.
- Perte granulo : les pertes grossières et fines doivent rester sous 0,3 %.
La saisie des résultats fait avancer l'éprouvette automatiquement : tout essai non conforme la marque Non-conformité ; un ensemble complet et conforme la marque Terminé. Un statut fixé à la main par le laboratoire n'est autrement pas touché. Chaque changement est écrit dans l'historique de l'éprouvette, champ par champ.
Moules Proctor¶
Le volume et la masse à vide des moules sont des propriétés fixes, revérifiées environ une fois par an, donc configurées une seule fois dans Paramètres (barre du haut) plutôt que ressaisies à chaque essai. Chaque résultat enregistré fige un instantané du volume et de la masse avec lesquels il a été calculé, de sorte que revérifier ou retirer un moule ne modifie jamais un résultat déjà existant.
Un Proctor lit aussi les valeurs de passants à 5 mm et 20 mm depuis la granulométrie de la même éprouvette ; elles orientent la suggestion de méthode/moule et la correction pour gros granulats. En pratique, la granulométrie est toujours réalisée en premier.
Configuration¶
Deux sources selon la façon dont l'application tourne :
- Docker / Coolify : variables d'environnement (ci-dessous).
docker-compose.ymlles lit depuis.env. - En local avec
dotnet run:src/LabTracker.Web/appsettings.Development.json, exclu de git, contenant les sectionsConnectionStrings:Default,Odk,AzureAdetPortail.
Ne jamais commiter de secrets. Le fichier de développement est exclu de git volontairement.
| Variable | Rôle |
|---|---|
DB_PASSWORD |
Mot de passe Postgres |
ODK_BASE_URL, ODK_EMAIL, ODK_PASSWORD, ODK_PROJECT_ID, ODK_SYNC_INTERVAL |
Compte de service ODK Central et cadence de synchronisation |
AzureAd__Instance, AzureAd__TenantId, AzureAd__ClientId, AzureAd__ClientSecret |
Inscription de l'application Azure AD |
PORTAIL_BASE_URL, PORTAIL_TOKEN, PORTAIL_ENABLED |
Point d'accès et jeton de service du portail de rapports |
Le processus force la culture fr-CA pour que les dates et les nombres soient formatés de façon cohérente, peu importe la locale de l'hôte. Derrière un reverse proxy, ForwardedHeaders est configuré pour que les redirections OIDC se résolvent correctement.
Si un proxy d'entreprise effectuant une inspection TLS brise la connexion ODK avec UntrustedRoot, définir "Odk:AllowUntrustedCertificate": true dans le fichier de développement. Jamais en production.
Rapports¶
Deux rapports PDF, tous deux reproduisant les formulaires papier existants du laboratoire, générés avec QuestPDF depuis Core/Reports/.
| Rapport | Portée | Point d'accès |
|---|---|---|
| Ruptures des cylindres de béton (CAN/CSA-A23.2-9C) | par projet | GET /reports/ruptures/{projectNumber} |
| Rapport d'analyse sur sols (LC 21-040 + Proctor) | par éprouvette | GET /reports/granulo/{specimenId} |
Les deux peuvent aussi être transmis au portail depuis la fenêtre de l'éprouvette.
Points qu'une personne qui découvre le projet va rencontrer :
- Le rapport béton imprime une ligne par échantillon, pas par cylindre : les relevés à l'état frais, le bris à 7 jours, les deux bris à 28 jours, leur moyenne, et la moyenne cumulative de ces moyennes. La moyenne cumulative de la première ligne est toujours des tirets.
- Un projet peut référencer plusieurs devis de béton, alors que le formulaire ne prévoit qu'un seul bloc de spécification ; le rapport émet donc une page par devis.
- Les valeurs sous leur exigence sont marquées
**, expliquées par la légende sous le tableau. - Le logo d'en-tête est une ressource intégrée (
Reports/Assets/logo-entete.png), pour que l'image de marque ne puisse pas manquer dans un conteneur.
Envoi au portail de rapports¶
Les rapports sont transmis au portail central de l'entreprise (lcl-rapports). L'interface est spécifiée dans docs/contrat-envoi-rapports.md, la source de vérité partagée, reflétée du côté du portail. À lire avant de modifier quoi que ce soit ici.
Le mécanisme est une outbox : mettre un rapport en file d'attente écrit une ligne ReportDelivery contenant le PDF, et ReportDeliveryWorker l'envoie en arrière-plan, un à la fois.
- Une panne du portail retarde un rapport, elle ne le perd jamais.
- Chaque rapport a un
sourceIdstable, donc renvoyer un rapport corrigé le remplace plutôt que de le dupliquer. - Les erreurs sont classées en réessayables ou non : une requête mal formée n'est jamais réessayée indéfiniment, et un incident réseau ne fait jamais perdre un rapport. Un projet inconnu est réessayé selon un horaire étalé, car le portail peut simplement ne pas encore le connaître.
- Chaque envoi déclare explicitement ses non-conformités, liste vide incluse ; le portail se fie à cette déclaration plutôt que d'analyser le PDF.
L'envoi est manuel aujourd'hui (un bouton par rapport). L'envoi automatique à la complétion est conçu mais pas activé. Paramètres → Rapports remet en file d'attente tous les rapports déjà envoyés, pour repeupler le portail après que son dossier a été vidé.
Tableau de bord¶
- Liste d'éprouvettes filtrable et triable : cliquer sur l'en-tête d'une colonne pour trier, sur l'icône de filtre pour ouvrir un panneau.
- Les filtres se combinent : numéro d'échantillon, type, projet, plage de dates de soumission, statut, priorité, non-conformité, laboratoire et réception physique.
- Des cartes de mesures, plus un panneau de charge de travail par laboratoire, extensible jusqu'au détail par éprouvette.
- La vue par défaut trie par échéance et cache Terminé, pour que le travail terminé disparaisse de la liste.
- Cliquer sur une ligne ouvre la fenêtre de l'éprouvette : Détails (données ODK en lecture seule + boutons de rapport), Historique (audit complet), Modifier (statut, priorité, échéance, laboratoire, réception, notes, essais, numéro d'échantillon manuel), Résultats (saisie des essais, calcul en direct, conformité, graphiques).
- Un manuel d'utilisation se trouve à
/manuel; les réglages s'ouvrent depuis la barre du haut.
Organisation du dépôt¶
labtracker/
├── docker-compose.yml Postgres + app, for Coolify
├── .env.example
├── docs/
│ └── contrat-envoi-rapports.md Interface contract with the report portal
├── tests/
│ └── LabTracker.Core.Tests/ xUnit tests for the calculation engine
└── src/
├── LabTracker.Core/
│ ├── Entities/ OdkProject, OdkSample, SampleSpecimen, SpecimenTest, TestResult,
│ │ BetonEssai, ProctorMould, ReportDelivery, AuditEntry,
│ │ OdkDatasetConfig, Beton/Granulaires/EnrobeSpec
│ ├── Enums/ SampleType, EntityKind, LabTrackingStatus, Priority, Labo,
│ │ TestType, ResultConformity, DeliveryStatus
│ ├── Data/ AppDbContext
│ ├── Lab/ Deadline strategies, DeadlineService, TestCatalog, WorkloadService
│ │ └── Results/ The calculation engine (see above)
│ ├── Reports/ RuptureReportGenerator, GranuloReportGenerator, Assets/
│ │ └── Portal/ Outbox delivery: client, service, worker, NC declaration
│ ├── Odk/ OdkCentralClient, SyncWorker, FreshConcreteApplier, OdkOptions
│ │ └── Mappers/ IEntityMapper + one mapper per entity kind + MapperHelpers
│ └── Migrations/
└── LabTracker.Web/
├── Program.cs DI, auth, startup migration, dataset + mould seeding, endpoints
├── DesignTimeDbContextFactory.cs Lets `dotnet ef` run without booting the host
├── Components/
│ ├── Pages/ Dashboard, Manual, Error, NotFound
│ └── Shared/ SampleDetailDialog, ResultsTab, SettingsDialog
└── wwwroot/app.css Custom dark theme
Travailler sur le projet¶
Migrations de base de données¶
Les migrations sont appliquées automatiquement au démarrage, donc il est rare de devoir lancer quelque chose à la main. Pour en ajouter une après un changement de modèle :
cd src/LabTracker.Web
dotnet ef migrations add MyChange --project ../LabTracker.Core --startup-project .
Deux points pratiques :
- Si l'application tourne, la compilation échoue sur une DLL verrouillée. Ajouter
--configuration Releasepour compiler vers un autre dossier de sortie. - Ne jamais modifier une migration déjà appliquée. En ajouter une nouvelle à la place ; c'est l'historique appliqué qui correspond à la base de données.
Tests¶
dotnet test couvre le moteur de calcul, les règles de conformité, les fuseaux granulométriques, la dérivation du statut et le contenu envoyé au portail. Quand une formule change, modifier ou ajouter un test avec elle : plusieurs ont été écrits en reproduisant une valeur connue tirée des chiffriers du laboratoire, et ce sont eux qui prouvent que la transposition est fidèle.
Conventions¶
- Commentaires en anglais ; le français n'est gardé que pour le vocabulaire du domaine (béton, éprouvette, granulaires) et pour les libellés cités de l'interface ou des rapports.
- Les commentaires expliquent pourquoi, pas ce que le code dit déjà.
- Les enums sont persistées sous forme de chaînes, pour que les valeurs en base survivent à une réorganisation.
AppDbContextest résolu via une factory : les composants Blazor et les workers en arrière-plan créent des contextes hors d'une portée de requête.
Limites connues¶
- Enrobé : le préfixe de champ
ee_*est extrapolé à partir des conventions du béton et des granulats, et n'a jamais été vérifié contre une vraie soumission. Aucune saisie de résultat non plus. - Les règles d'échéance pour les granulats et l'enrobé sont provisoires en attendant les délais du client ; la règle du béton reste aussi à confirmer.
- Les calculs de granulométrie devraient encore être validés de bout en bout contre un jeu de données de référence rempli.
- Proctor : la densité sèche maximale corrigée reproduit littéralement la formule du chiffrier, et cette formule donne une valeur invraisemblable (elle mélange des pourcentages là où la norme utilise des fractions). Laissée telle quelle délibérément, en attente d'une décision.
- L'envoi automatique des rapports est conçu mais pas activé.
- Mise à jour en temps réel : le tableau de bord se rafraîchit sur ↻ ou après un enregistrement ; SignalR pourrait pousser les mises à jour.
SampleTrackingetBetonResistanceCalculatorne sont plus utilisés par l'application en production. Les deux sont conservés intentionnellement ; ne les supprimer que si on est certain qu'ils ne reviendront pas.