Module — Supervision (état des services)
Synchronisé avec le code. Mettre à jour à chaque changement structurel.
Rôle
Savoir, sans attendre qu'un utilisateur se plaigne, si les services externes dont Horizon dépend répondent : Visual Planning, Business Central, Saferpay, CembraPay, les envois d'emails, la recherche du site. Complète le monitoring des jobs du worker (/api/health/workers, cf. architecture/overview.md).
Motivation (août 2026) : Visual Planning répondait en ~100 s sans timeout, ce qui plantait l'éditeur de réservation. Le correctif dégrade désormais en silence (cf. resources-visual-planning.md > Timeout et dégradation) — cette page est la contrepartie de ce silence : plus aucune interface ne remonte l'erreur, mais l'état est visible ici et notifié.
Emplacement
- Sondes et service :
src/Application/Services/Health/(ServiceHealthProbe,ServiceHealthService,HealthServices,Probes/) - Entité :
src/Domain/Entities/ServiceHealthCheck.cs, enumDomain/Enum/ServiceHealthStatus.cs - Page :
src/Web/Pages/ServicesStatus/Index.cshtml(.cs)— menu Système > État des services,[Authorize(Roles = "Administrator,Direction")] - API :
src/Web/Areas/Api/HealthController.cs→GET /api/health/services - Job :
BackgroundWorker.DoServicesHealthJob, toutes les 5 min (Workers.ServicesHealth) - Tests :
tests/Application/Services/ServiceHealthServiceTest.cs
Fonctionnement
Sondes
Une classe par service, dérivée de ServiceHealthProbe, qui expose Service / Category / IsCritical et implémente ProbeAsync. La classe de base mesure le temps de réponse, attrape toute exception et la transforme en Down : une sonde ne peut jamais faire tomber la page ni le job.
| Service | Sonde | Critique |
|---|---|---|
| Base de données (SQL Server) | Database.CanConnectAsync | oui |
| Visual Planning | resources?resourceModel=ETAPE&count=1 (le plus petit référentiel VP, ~1.5 Ko) | oui |
| Business Central | jeton Azure AD (cache ~1 h de BcSoapService) + GET du WSDL de WS/SystemService?tenant=… | oui |
| Saferpay | Transaction/Inquire sur une transaction inexistante → 402 TRANSACTION_NOT_FOUND = API debout et identifiants acceptés, 401 = identifiants refusés | oui |
| CembraPay | jeton Azure AD client-credentials + joignabilité de BaseUrl | oui |
| SendGrid | GET /v3/scopes (droits de la clé, hors quota d'envoi) | oui |
| BetterSearch | GET /travels?search=a&size=1, le vrai endpoint de TravelController | non |
| Gender-API | joignabilité seule — chaque appel authentifié consomme le quota du forfait | non |
| Google Maps | joignabilité seule — l'API est facturée à la requête | non |
Codes HTTP vérifiés en vrai (18.08.2026), deux pièges à ne pas réintroduire :
- Business Central : la racine des web services SOAP (
GetBaseWebServiceUrl(),/WS/{Company}) renvoie 500 — ce n'est pas une ressource interrogeable, c'était le premier essai et il affichait BC en panne alors qu'il tournait.WS/SystemService?tenant=…renvoie 200 (WSDL) avec un jeton valide et 401 sans, sans dépendre d'une page publiée : c'est ce qui s'en rapproche le plus d'un endpoint de santé côté BC.BcOptions.GetSystemServiceUrl(). - Saferpay : une erreur métier sort en 402 (
{"ErrorName":"TRANSACTION_NOT_FOUND"}), pas en 400 — d'où un faux « hors service » au premier essai. Des identifiants faux sortent bien en 401, la sonde distingue donc réellement les deux cas.
⚠ Aucune sonde n'a d'effet de bord : pas d'email de test, pas de transaction de test, pas d'écriture. C'est la règle à respecter pour toute sonde ajoutée.
Statuts
Up / Degraded (répond mais au-delà de DegradedAboveMs, 3 s par défaut, 1 s pour SQL) / Down / NotConfigured (clé ou URL absente de l'environnement — ni disponible ni en panne, exclu du calcul de disponibilité).
Deux garde-fous de temps
- Chaque sonde est plafonnée à 5 s (
ServiceHealthProbe.MaxDuration), via unTask.WhenAnycontre unTask.Delay— et pas seulement via leCancellationToken, parce qu'une dépendance peut l'ignorer (BcSoapService.GetAccessTokenAsyncn'en prend pas). Au-delà, le service estDownet la sonde abandonnée continue dans le vide, exception observée pour ne pas remonter enUnobservedTaskException. - Toutes les sondes tournent en parallèle (
Task.WhenAll) : la page répond en ~5 s dans le pire cas, jamais en somme des sondes.
La page charge d'ailleurs son tableau après rendu (handler ?handler=Check en fetch, auto-refresh 60 s) : l'écran s'affiche immédiatement même si un service met 5 s.
Historique et alerting
DoServicesHealthJob(worker, 5 min) appelleCheckAllAsyncpuisRecordAsync: une ligneServiceHealthCheckpar service et par tour, purgée au-delà de 30 jours (par lots de 1000, le worker repasse).GetHistoryAsync(days = 7)calcule le taux de disponibilité et regroupe les relevésDownconsécutifs en une seule coupure (affichée « du … au … (n relevés) »).- Notification
Administratorsur bascule uniquement — quand un service critique tombe, et quand il revient. In-app uniquement, jamais par mail (choix Buchard, août 2026) : une panne de service se regarde depuis Horizon. L'état précédent est relu sur les 2 dernières heures : worker arrêté plus longtemps ⇒ une notification de rappel, jamais de spam à chaque tour.
Endpoint de monitoring externe
GET /api/health/services — [AllowAnonymous], 200 si aucun service critique n'est Down, 503 sinon. Réponse mise en cache 30 s : l'endpoint est anonyme et déclenche un appel sortant par service, sans cache un monitoring bavard martèlerait Saferpay, BC et VP. Le message de la sonde en est volontairement absent (pas d'URL ni de détail interne exposé), contrairement à la page back-office.
Ajouter un service surveillé
- Une classe dans
Application/Services/Health/Probes/dérivée deServiceHealthProbe. - Une ligne dans
HealthServices.Configure— seul endroit à toucher, Web et Worker partagent cette liste. - Si l'option de configuration n'existe pas encore côté worker, l'ajouter dans
WorkerHostBuilder(sinon la sonde y seraNotConfiguredalors qu'elle est verte côté web).
Points d'attention / pièges
- Ne jamais utiliser
ExecuteDeleteAsyncdans ce service : le provider InMemory des tests ne le supporte pas. La purge passe parRemoveRangesur un lot borné. - Le worker et le web ont deux DI distinctes : un service surveillé côté web mais non configuré côté worker produira un historique
NotConfiguredalors que la page afficheUp. Workers.ServicesHealthest surveillé par/api/health/workersavec une tolérance de 15 min (3 intervalles).- La migration EF
ServiceHealthChecksdoit être créée et appliquée avant que la page d'historique et le job ne fonctionnent.
Dépendances
- ←
resources-visual-planning(sonde VP, contrepartie de la dégradation silencieuse). - ←
billing-payment(sondes Saferpay / CembraPay / Business Central). - →
notes-notifications(notificationAdministratorsur bascule).

