ScoroOdkBridge¶
Service d'arrière-plan (.NET 10 Worker Service, sans UI ni endpoint HTTP) qui fait le pont entre Scoro (gestion de projets) et ODK Central (collecte de données terrain) pour LCL Environnement.
À intervalle régulier, le service interroge l'API Scoro pour les projets récemment modifiés, filtre ceux qui concernent le contrôle des matériaux, et crée une entity correspondante dans un dataset ODK Central, pour que les équipes de terrain puissent remplir des formulaires ODK rattachés à ces projets. Le pont ne fait que créer des entities ; il ne les modifie ni ne les supprime jamais.
Ce dépôt fait partie de l'infrastructure LCL Environnement décrite dans
l'Infrastructure (un seul droplet, Coolify, Caddy,
lclchantiers.com). Ce service tourne en arrière-plan uniquement : il n'a pas
de sous-domaine.
Comment ça marche¶
À chaque cycle (Worker.cs) :
- Charge le fichier de suivi local (
ProjectTracker) des IDs Scoro déjà traités. - Interroge Scoro (
ScoroClient.GetProjectsModifiedAsync) pour les projets modifiés depuisLookbackMinutesminutes. - Filtre les projets éligibles : nom de projet préfixé
CM-ouCMC-(ProjectMapper.IsEligible).CML-est volontairement exclu pour l'instant. - Pour chaque projet éligible pas encore dans le tracker, construit le
payload d'entity (
ProjectMapper.ToEntity) et le crée dans ODK Central (OdkClient.CreateEntityAsync). - Marque le projet comme traité dans le tracker dès la création réussie.
- Une erreur ODK sur un projet est loguée et n'interrompt ni le cycle ni le
projet suivant (
OdkApiExceptioncatché par projet). Une exception inattendue interrompt le cycle en cours mais pas la boucle du worker : elle est loguée et le prochain cycle démarre normalement au tick suivant.
Le premier cycle tourne immédiatement au démarrage, puis à chaque tick d'un
PeriodicTimer (intervalle = PollIntervalMinutes, 20 min par défaut).
Architecture¶
Trois dossiers indépendants, assemblés dans Program.cs :
Scoro/ client de l'API Scoro (lecture seule)
ODK/ client de l'API entities d'ODK Central (création seule)
Bridge/ la colle : filtre d'éligibilité, mapping, fichier de suivi
Scoro/¶
ScoroClient: POST versprojects/list(paginé,filter.modified_date) etprojects/view/{id}. La clé d'API Scoro est envoyée comme champ du corps de la requête (apiKey), pas comme header (voirScoroClient.BuildBody).ScoroServiceCollectionExtensions: pipeline de résilience (Microsoft.Extensions.Http.Resilience/ Polly) : retry sur 429/503 et erreurs de transport, jusqu'à 5 tentatives, backoff exponentiel avec jitter. Respecte le headerx-ratelimit-resetde Scoro (ouRetry-After) quand présent au lieu du backoff par défaut. Timeout de 20 s par tentative.ScoroProject: désérialise la formeprojects/list/projects/view, y compris le tableaucustom_fields, aplati dans une map insensible à la casse (CustomFieldsMap) exposée via des accesseurs typés :SiteAdress(c_adresse_site),City(c_ville),PrescribedMandate(c_mandat_prescrit),Sharepoint(c_sharepoint),Referencer(c_referencer). UnFlexibleStringConvertercoerce les champs personnalisés numériques/booléens Scoro enstring, et les valeurs texte sont HTML-décodées (ex.Ayer's Cliff→Ayer's Cliff).ScoroResponse<T>: enveloppestatus/statusCode/messages/datacommune aux réponses Scoro ;EnsureOklève uneScoroApiExceptionsistatus != OK.
ODK/¶
OdkClient:CreateEntityAsync(POSTv1/projects/{ProjectId}/datasets/{DatasetName}/entities) etGetExistingScoroIdsAsync(lecture OData paginée$top/@odata.nextLinksur le dataset, utilisée pour reconstruire le tracker si besoin).OdkSessionHandler:DelegatingHandlerqui gère l'authentification par jeton de session : connexion email/mot de passe au premier appel, jeton mis en cache, rafraîchi 30 min avant expiration, un seul retry automatique sur 401 (jeton révoqué côté serveur).OdkServiceCollectionExtensions: enregistre leHttpClienttypé avec le handler de session. En développement uniquement (IHostEnvironment.IsDevelopment()), fait confiance au certificat auto-signé d'ODK local, jamais activé en production.
Bridge/¶
ProjectMapper.IsEligible: seule source de vérité pour savoir quels projets Scoro sont pontés.ProjectMapper.ToEntity: construit le payload(label, data)de l'entity :scoro_id,project_no,company_name,address,city,prescribed_mandate,start_date,end_date.label= nom du projet Scoro (affiché dans le menu déroulant du formulaire ODK).ProjectTracker: ensemble d'IDs Scoro déjà pontés, persisté en JSON (data/tracking.jsonpar défaut, chemin configurable). Les écritures passent par un fichier temporaire puis unFile.Moveatomique pour survivre à un crash en cours d'écriture.SeedAsync(actuellement non appelé automatiquement) permet de reconstruire le tracker à partir des entities existantes dans ODK viaOdkClient.GetExistingScoroIdsAsync.
Le fichier de suivi est critique¶
data/tracking.json doit vivre sur un volume persistant en production.
S'il est perdu ou vidé, le pont va tenter de recréer en entity ODK tous
les projets éligibles historiques au prochain cycle, créant des doublons.
Ne jamais le supprimer ou le réinitialiser sans d'abord le reconstruire via
GetExistingScoroIdsAsync / ProjectTracker.SeedAsync.
Prérequis¶
- .NET SDK 10
- Accès réseau à l'API Scoro (
https://lcl.scoro.com/api/v2) et à l'instance ODK Central cible
Configuration¶
Liée via le pattern Options (ValidateDataAnnotations().ValidateOnStart())
sur trois sections : Scoro, Odk, Bridge.
| Section | Clé | Requis | Défaut | Description |
|---|---|---|---|---|
Scoro |
BaseUrl |
non | https://lcl.scoro.com/api/v2 |
URL de base de l'API Scoro |
Scoro |
ApiKey |
oui | - | Clé d'API Scoro |
Scoro |
CompanyAccountId |
oui | - | Identifiant de compte Scoro |
Scoro |
Lang |
non | fre |
Langue des réponses Scoro |
Scoro |
PageSize |
non | 25 |
Taille de page pour projects/list |
Odk |
BaseUrl |
oui | - | URL de base d'ODK Central |
Odk |
Email |
oui | - | Email du compte de service ODK |
Odk |
Password |
oui | - | Mot de passe du compte de service ODK |
Odk |
ProjectId |
non | 1 |
ID du projet ODK Central cible |
Odk |
DatasetName |
non | scoro_projects |
Nom du dataset ODK cible |
Bridge |
TrackingFilePath |
oui | data/tracking.json |
Chemin du fichier de suivi (volume persistant en prod) |
Bridge |
LookbackMinutes |
non | 60 |
Fenêtre de recherche des projets modifiés (chevauche l'intervalle de poll par sécurité) |
Bridge |
PollIntervalMinutes |
non | 20 |
Intervalle entre deux cycles |
appsettings.json contient la structure avec des secrets vides.
appsettings.Development.json contient des valeurs de dev locales (URL ODK
https://localhost). En production, les secrets viennent des variables
d'environnement Coolify, jamais codés en dur (règle du CLAUDE.md racine).
Le mapping standard ASP.NET Core s'applique, ex. Scoro__ApiKey,
Odk__Password.
Un UserSecretsId est déjà configuré dans le .csproj : pour du dev local
sans toucher appsettings.Development.json, dotnet user-secrets set
"Odk:Password" "..." fonctionne aussi.
Lancer le projet¶
dotnet build
dotnet run # démarre la boucle du worker (poll toutes les PollIntervalMinutes)
dotnet run -- --selftest # test de parsing JSON hors-ligne, aucun appel réseau, puis quitte
dotnet run -- --scoro-test # appelle la vraie API Scoro avec les creds configurées, affiche les projets correspondants, puis quitte
Il n'y a pas de projet de test dédié. Scoro/Tests/ScoroParsingSelfTest.cs
est un self-test artisanal (assertions Check(label, bool) affichées en
PASS/FAIL sur stdout) qui valide la désérialisation JSON de
ScoroProject/ScoroResponse contre un exemple de payload
projects/list. Lancer --selftest après toute modification sous
Scoro/ qui touche au parsing (mapping des champs personnalisés,
HTML-decoding, coercion numérique→string, etc.).
Docker¶
Build multi-étapes, correspond à la production :
docker build .
mcr.microsoft.com/dotnet/sdk:10.0 (build) →
mcr.microsoft.com/dotnet/runtime:10.0 (exécution), point d'entrée
dotnet ScoroOdkBridge.dll. En production (Coolify), monter un volume
persistant sur le chemin de Bridge:TrackingFilePath et fournir toutes les
variables d'environnement de secrets (voir la section Configuration).
Problème connu : secrets commités¶
appsettings.Development.json est suivi par git (seul .dockerignore
l'exclut ; .gitignore ne le fait pas) et contient une vraie clé d'API Scoro
et un vrai mot de passe du compte de service ODK Central, en clair. Ceci
viole la règle « jamais de secret dans le code » du CLAUDE.md racine.
Ne pas aggraver en commitant d'autres vrais secrets dans des fichiers de config. Ceci doit être traité comme une remédiation à faire, pas comme un état normal :
- Retirer
appsettings.Development.jsondu suivi git et l'ajouter à.gitignore. - Purger le secret de l'historique git.
- Faire tourner (rotate) la clé Scoro et le mot de passe du compte de service ODK compromis.
Notes de collaboration¶
- Ne jamais dupliquer la logique déjà couverte par le fichier de suivi : il empêche les doublons et vit sur un volume persistant à ne jamais effacer sans comprendre pourquoi.
- Le pont ne traite que les projets
CM-/CMC-;CML-est délibérément exclu (voirProjectMapper.IsEligible). - Le pont ne fait que créer des entities ODK, jamais modifier ni supprimer.
- Se réveille toutes les
PollIntervalMinutes(20 min par défaut).