57 KiB
ARCHITECTURE PROJET OMIKA — Guide complet (pour comprendre et débugger)
Ce document explique comment fonctionne chaque application du dépôt Omika, à quoi sert chaque projet, et où chercher quand un bug apparaît. Il a été rédigé en lecture seule (aucun code modifié), à partir du code source réel et de la documentation existante (
docs/*.md).Public visé : développeur débutant qui doit retrouver la cause d'un bug.
Table des matières
- Le vocabulaire indispensable
- Vue d'ensemble en une image
- Les deux générations (legacy WINDEV → moderne .NET)
- La carte des 13 projets
- Le graphe des dépendances
- Les bases de données et connexions
- Flux 1 : démarrage de l'application
- Flux 2 : activation de licence
- Flux 3 : synchronisation mobile → serveur
- Flux 4 : snapshot serveur → mobile
- Flux 5 : documents (upload / download)
- L'authentification par token
- La configuration (où sont les réglages)
- Les codes d'erreur et états
- Guide de diagnostic : symptôme → où chercher
- Les points de trace / logs
- Les pièges et limites connus
1. Le vocabulaire indispensable
- Solution (
.slnx) : le conteneur qui regroupe tous les projets. IciOmika.slnx. - Projet (
.csproj) : une "brique" de code qui se compile en un exécutable (.exe) ou une bibliothèque (.dll). - Bibliothèque : du code réutilisable qui ne démarre pas tout seul (ex.
Omika.Core). - Exécutable / app : un programme qui démarre (ex.
Omika.Api,Omika.Worker,Omika.App.Desktop). - Couche (layer) : découpage logique du code (UI, métier, données…).
- API / Web service : un serveur qui répond à des requêtes HTTP (pour des programmes, pas des humains). Ici
Omika.Api. - Worker / Service : programme sans fenêtre qui tourne en boucle en arrière-plan. Ici
Omika.Worker. - Endpoint : une "porte" précise de l'API, ex.
POST /api/sync/operations. - DI (injection de dépendances) : technique pour assembler automatiquement les briques.
- Scoped / Singleton / HostedService : durées de vie des services en DI (par requête / unique / arrière-plan).
- Repository : objet chargé de lire/écrire dans une base de données.
- Service applicatif : objet qui contient la logique métier (règles), indépendant de l'UI et de la base.
- DTO (Data Transfer Object) : objet "plat" qui transporte des données entre deux couches.
- Entité : objet métier qui représente une donnée du domaine (Client, Intervention…).
- HFSQL : la base de données historique (PC SOFT), accessible via ODBC ou OLE DB.
- SQLite : base locale embarquée dans le téléphone (mode hors-ligne).
- Sync : mécanisme qui fait transiter les données téléphone ↔ serveur.
- Outbox : "boîte d'envoi" locale qui met en file les changements à envoyer au serveur.
- Snapshot : "photo" complète des données métier renvoyée au mobile pour reconstruire sa base locale.
- Idempotence : rejouer la même opération ne produit pas d'effet en double (grâce à un
OperationIdunique).
2. Vue d'ensemble en une image
┌──────────────────────────────────────────────┐
│ TÉLÉPHONE / BUREAU │
│ │
│ Omika.App.Desktop / Android / iOS (l'UI) │
│ │ utilise │
│ ▼ │
│ Omika.App (Vues + ViewModels + Navigation) │
│ │ utilise │
│ ▼ │
│ Omika.App.Local (SQLite + SyncEngine) │
└──────────────────┬───────────────────────────┘
│ HTTP (JSON + Bearer token)
▼
┌──────────────────────────────────────────────┐
│ SERVEUR (HFSQL) │
│ │
│ Omika.Api (web service) ──► écrit TOSRV │
│ │ utilise │
│ ▼ │
│ Omika.Worker (service de sync) │
│ │ TOSRV → MobReception → handler │
│ │ → MobLog │
│ ▼ │
│ Omika.Core (métier) + Omika.Data (HFSQL) │
└──────────────────────────────────────────────┘
À retenir absolument :
- Le mobile ne parle jamais directement à HFSQL : il passe toujours par
Omika.Api. - Le desktop accède à HFSQL directement (historique).
Omika.Apireçoit les opérations ;Omika.Workerles applique.Omika.Coreest le cerveau métier ;Omika.Dataest la main qui touche HFSQL.
3. Les deux générations
| Génération 1 (legacy) | Génération 2 (ce dépôt) | |
|---|---|---|
| Langage | WINDEV | C# / .NET 10 |
| Base de données | HFSQL (serveur) | HFSQL (serveur) + SQLite (mobile) |
| Interface | Fenêtres WINDEV | Avalonia (Desktop / Android / iOS) |
| Web service | Batik_WS_Com_Mob, Batik_WS_Lic_Mob |
Omika.Api (ASP.NET Core) |
| Service de sync | Omika_S_Comm_DEV (WINDEV) |
Omika.Worker (.NET) |
Les fichiers docs/BATIK.md, docs/OMIKA.md, docs/Documentation_Service.md,
docs/Documentation_WebService_*.md sont des exports WINDEV de l'ancien
système. Ils servent uniquement de référence historique : on ne copie plus
leur code. La logique moderne fait foi.
⚠️ Règle d'or du projet : la base HFSQL existante n'est jamais modifiée (aucune table créée, supprimée ou changée). Le nouveau code ne fait que lire/écrire dans les tables déjà existantes.
4. La carte des 13 projets
Chaque projet est décrit avec son rôle et ses fichiers clés (les endroits où regarder en premier en cas de bug).
4.1 Omika.Core — le cerveau métier (indépendant de l'UI et de la base)
Chemin : src/Omika.Core/ — TargetFramework : net10.0 — Dépend de : rien.
| Dossier / fichier | Rôle |
|---|---|
Domain/Entities/ |
Les entités métier (Client, Intervention, Licence, Document…). |
Domain/ValueObjects/ |
Objets-valeur (petites notions métier immuables). |
Domain/Commands/ |
Commandes (intentions d'action). |
Domain/Exceptions/ |
Exceptions métier (ex. DomainValidationException). |
Contracts/ |
Les DTO échangés via l'API, organisés par domaine (Clients, Interventions, Mobile, Planning, Security…). |
Interfaces/ |
Les contrats (IClientService, IInterventionRepository, ISyncOperationService, ILicenceRepository…). |
Services/ |
L'implémentation des services métier (ClientService, InterventionService…) + le sous-dossier Services/Mobile. |
Services/Mobile/ |
Le cœur serveur de la sync mobile (voir détails ci-dessous). |
Configuration/ |
Options de configuration (MobileAuthOptions, LicenseOptions, SyncOptions, DevelopmentAuthOptions…). |
Constants/ |
Constantes (ApplicationConstants, LicenseErrorCodes, SyncEntityTypes…). |
Runtime/ |
ApplicationRuntimeContext : décide Batik vs Omika. |
ServiceCollectionExtensions.cs |
Enregistre les services mobile en DI (AddOmikaMobileServices). |
Services/Mobile/ en détail (les fichiers les plus importants du serveur) :
| Fichier | Rôle |
|---|---|
LicenseService.cs |
Vérifie et active/valide une licence dans HFSQL. |
TokenService.cs |
Fabrique et valide les jetons d'authentification (HMAC-SHA256). |
MobileBootstrapService.cs |
Construit le "bootstrap" (profil, droits, collaborateur, licence, paramètres). |
MobileSnapshotService.cs |
Construit le snapshot complet des données métier. |
MobileDocumentService.cs |
Upload/download des documents (via IDocumentFileStorage). |
SyncOperationService.cs |
Reçoit les opérations de sync et les écrit dans TOSRV. |
MobileIdentityProvider.cs |
Fournit le profil et les droits (Batik vs Omika). |
MobileEntityIdentityResolver.cs |
Convertit l'identité publique (GUID mobile) ↔ identité physique (PK HFSQL). |
MobileIdentityMapper.cs |
Aide à la résolution d'identité. |
4.2 Omika.Data — la main qui touche HFSQL
Chemin : src/Omika.Data/ — Dépend de : Omika.Core.
| Dossier / fichier | Rôle |
|---|---|
Connection/ |
Les connexions HFSQL (ODBC + OLE DB PCSOFT.HFSQL). |
Configuration/ |
HfsqlOptions + validateurs (HfsqlOptionsValidator, etc.). |
Repositories/Adaptive*.cs |
Repositories adaptatifs : lisent Batik OU Omika (noms de colonnes différents). |
Repositories/Batik/ et Repositories/Omika/ |
Les variantes par génération de base. |
Repositories/Technical/LicenceRepository.cs |
Accès aux tables de licence (ClientBase, Licence, LicenceHisto). |
Repositories/Technical/SyncTransferRepository.cs |
Accès aux tables de sync (TOSRV, MobReception, MobLog). |
HfSql/Models/ |
Modèles physiques générés des tables HFSQL (106 classes). |
HfSql/Mapping/ |
Attributs de mapping (Table, Column, PrimaryKey…). |
SchemaDetection/ |
Détecte le schéma réel de la base connectée. |
Runtime/ |
Initialisation du runtime (validation de la base). |
Migrations/ |
Migration Batik → Omika. |
ServiceCollectionExtensions.cs |
La clé du branchement : AddOmikaData(), AddOmikaDataLicenseConnection(), AddOmikaDataSyncConnection(), AddOmikaDataServiceConnection(), AddOmikaDataBusinessOleDb(). |
4.3 Omika.Api — le web service moderne
Chemin : src/Omika.Api/ — SDK : Microsoft.NET.Sdk.Web — Dépend de : Omika.Core, Omika.Data.
| Fichier | Rôle |
|---|---|
Program.cs |
Le point d'entrée : config, DI, détection de base, endpoints. |
MobileAuthMiddleware.cs |
Valide le token Bearer et stocke l'identité dans HttpContext.Items. |
Endpoints/*.cs |
Tous les endpoints (voir liste ci-dessous). |
Endpoints métier : ClientEndpoints, ContactEndpoints, SiteEndpoints,
InterventionEndpoints, DossierEndpoints, RendezVousEndpoints,
CollaborateurEndpoints, DocumentEndpoints, ReferenceDataEndpoints.
Endpoints mobile :
LicenseEndpoints→POST /api/license/activate,POST /api/license/validateMobileBootstrapEndpoints→GET /api/mobile/bootstrapSyncEndpoints→POST /api/sync/operations,GET /api/sync/status/{id},GET /api/sync/snapshotMobileDocumentEndpoints→POST /api/documents,GET /api/documents/{guid}
Endpoints techniques : HealthEndpoints (/health), DevelopmentAuthEndpoints
(POST /api/dev/session, actif uniquement en Development).
4.4 Omika.Worker — le service de synchronisation (arrière-plan)
Chemin : src/Omika.Worker/ — SDK : Microsoft.NET.Sdk.Worker — Dépend de : Omika.Core, Omika.Data.
| Fichier | Rôle |
|---|---|
Program.cs |
Point d'entrée du Host .NET, enregistre les services et le worker. |
SyncWorker.cs |
BackgroundService : la boucle infinie (cycle + délai). |
SyncProcessor.cs |
La logique de traitement testable (transfert + application). |
SyncOperationHandlerRegistry.cs |
Le registre qui mappe un EntityType → handler. |
InterventionSyncOperationHandler.cs |
Applique les opérations Intervention (CREATE/UPDATE/ARCHIVE). |
SuiviTempsSyncOperationHandler.cs |
Applique les opérations SuiviTemps. |
DefaultSyncOperationHandler.cs |
Fallback : refuse proprement (NO_HANDLER), jamais de faux SUCCESS. |
4.5 Omika.App.Local — la couche locale "hors-ligne" du mobile
Chemin : src/Omika.App.Local/ — Dépend de : Omika.Core.
| Dossier / fichier | Rôle |
|---|---|
Sqlite/ |
La base locale omika.db (schéma, connexion). |
Models/ |
Entités locales (LocalClient, LocalIntervention, OutboxOperation…). |
Repositories/ |
Accès SQLite : LocalRepository<T>, OutboxRepository, IdentityMappingRepository, DocumentCacheRepository, SyncStateRepository, UiPreferencesRepository, LocalBootstrapRepository. |
Services/SyncEngine.cs |
Le moteur de synchronisation (cycle complet). |
Services/MobileApiClient.cs |
Client HTTP typé vers l'API (Bearer token). |
Services/MobileSessionService.cs |
Gestion de la session (token sécurisé + bootstrap + période offline). |
Services/OutboxService.cs |
Met en file les changements locaux à envoyer. |
Services/SnapshotApplier.cs |
Applique le snapshot serveur à SQLite (transaction atomique). |
Services/MobileIdentityReconciliationService.cs |
Réconcilie les identités locales (GUID mobile ↔ GUID canonique). |
Services/DocumentCacheService.cs |
Cache local des documents. |
Services/NetworkConnectivity.cs |
Détecte si on est en ligne. |
Services/*LocalService.cs |
Services métier locaux (ClientLocalService, SiteLocalService, InterventionLocalService…). |
Services/DevelopmentLicense*.cs |
Licence de développement (Debug uniquement). |
4.6 Omika.App — l'interface utilisateur partagée (Avalonia)
Chemin : src/Omika.App/ — Dépend de : Omika.Core, Omika.App.Local — Packages : Avalonia 12.1.1, CommunityToolkit.Mvvm 8.4.2.
| Fichier / dossier | Rôle |
|---|---|
App.axaml / App.axaml.cs |
Le point d'entrée de l'application Avalonia (classe App). |
AppServices.cs |
Le "composition root" : construit tous les services à la main (pas de conteneur DI). |
Views/ |
Les écrans .axaml + code-behind (≈ 25 vues). |
ViewModels/ |
La logique de chaque écran (MVVM). |
ViewLocator.cs |
Fait le lien Vue ↔ ViewModel par convention de nommage. |
Navigation/ |
La navigation entre pages (NavigationService, AppPage…). |
Services/ |
Services de plateforme (ThemeService, PlatformPhoneLauncher, PlatformSecureTokenStore…). |
Styles/ |
Apparence (couleurs, typographie, contrôles). |
Assets/ |
Ressources (icônes…). |
4.7 Les hôtes plateforme (démarrage)
| Projet | Chemin | Rôle | Fichier clé |
|---|---|---|---|
Omika.App.Desktop |
src/Omika.App.Desktop/ |
Hôte Windows (WinExe) |
Program.cs (Main + BuildAvaloniaApp) |
Omika.App.Android |
src/Omika.App.Android/ |
Hôte Android (net10.0-android) |
MainActivity.cs, Application.cs |
Omika.App.iOS |
src/Omika.App.iOS/ |
Hôte iOS (net10.0-ios) |
AppDelegate.cs, Main.cs |
4.8 Omika.Tests — les tests automatiques
Chemin : tests/Omika.Tests/ — Framework : xUnit. Dépend de presque tous les projets.
Vérifie le comportement du code sans base HFSQL réelle pour la plupart des tests.
4.9 tools/ — outils de diagnostic (lecture seule)
Chemin : tools/ — Petits programmes "sondes" (Probe) pour explorer la base
HFSQL sans rien écrire. Dans la solution : Omika.HfSqlProbe, HfSqlModelAudit,
HfSqlOleDbAudit. Hors solution : ≈ 14 autres Omika.*Probe + scripts
PowerShell (probe_*.ps1, start_*_e2e.ps1) et Python (generate_models.py,
extract_schema_tables.py).
5. Le graphe des dépendances
Omika.Core ◄── Omika.Data ◄── Omika.Api (web service)
▲ ▲
│ Omika.Worker (service de sync)
│
Omika.Core ◄── Omika.App.Local ◄── Omika.App ◄── Omika.App.Desktop
◄── Omika.App.Android
◄── Omika.App.iOS
Omika.Tests → (utilise presque tout)
Omika.Corene dépend de personne (c'est la base).Omika.Datane dépend que deOmika.Core.Omika.ApietOmika.WorkerpartagentCore+Data.- L'UI (
App) dépend deCore+App.Local, jamais deData(le mobile ne touche jamais HFSQL directement).
6. Les bases de données et connexions
6.1 HFSQL (côté serveur)
Base historique, répartie en plusieurs bases physiques sur un serveur HFSQL
(ex. 127.0.0.1:4900). D'après les docs des "lots serveur" :
| Base HFSQL | Rôle | Connexion .NET |
|---|---|---|
BATIK_TEST / OMIKA_TEST |
Données métier | Business / WebService |
BATIK.01_BLS |
Table TOSRV (file d'attente) |
Service (OLE DB) |
| base métier WebService | Tables MobReception + MobLog |
WebService |
BASE_LICENCES |
Licences | License (OLE DB) |
Les 3 tables techniques de synchronisation :
| Table | Base | Rôle | Colonnes clés |
|---|---|---|---|
TOSRV |
Service | File d'entrée (opérations reçues du mobile) | TS_PKID, TS_Date, TS_Mobile, TS_CMD, TS_Buffer, TS_Lu, TS_JSON |
MobReception |
WebService | File de travail (en attente de traitement) | MR_PKID, MR_IDToSRV, MR_JSON, MR_CMD, MR_Buffer, MR_NumSerie, MR_Fait, MR_DateHeureRecep |
MobLog |
WebService | Journal des résultats terminaux | ML_PKID, ML_DateCreat, ML_Message, ML_Niveau |
Les tables de licence (base Licence) : ClientBase (CL_PKID, CL_Code, CL_Nom),
Licence (LI_*), LicenceHisto (LH_*).
6.2 SQLite (côté mobile)
Fichier omika.db dans le dossier de données de l'application. Contient :
- les tables métier (copie locale :
Client,Site,Intervention,Dossier,RendezVous,Collaborateur,DocumentMetadata,SuiviTemps,Personne) ; - les tables techniques (
Outbox,IdentityMapping,SyncState,DocumentCache,LocalBootstrap,UiPreferences).
6.3 Comment les connexions sont branchées (DI)
Tout se joue dans src/Omika.Data/ServiceCollectionExtensions.cs :
| Méthode | Effet |
|---|---|
AddOmikaData() |
Connexion Business/WebService (ODBC historique), repositories métier, détecteur de schéma, IWebServiceHfSqlConnection. |
AddOmikaDataLicenseConnection() |
Connexion License (OLE DB), branche ILicenceRepository. |
AddOmikaDataSyncConnection() |
Connexion Sync (OLE DB) — OBSOLÈTE, conservée pour compat. |
AddOmikaDataServiceConnection() |
Connexion Service (OLE DB), branche IServiceHfSqlConnection (table TOSRV). |
AddOmikaDataBusinessOleDb() |
Override : remplace IHfsqlConnection ODBC par OLE DB + ajoute IWebServiceTransactionContext. |
Important pour le debug : dans MS DI, le dernier enregistrement gagne. C'est pour cela que
AddOmikaDataBusinessOleDb()doit être appelé aprèsAddOmikaData().
7. Flux 1 : démarrage de l'application
Point d'entrée : src/Omika.App.Desktop/Program.cs
Main() [STAThread]
→ BuildAvaloniaApp().StartWithClassicDesktopLifetime(args)
→ AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
Ensuite src/Omika.App/App.axaml.cs :
Initialize()
→ AvaloniaXamlLoader.Load(this) (charge App.axaml)
→ AttachDeveloperTools() (Debug uniquement)
OnFrameworkInitializationCompleted()
→ _services = new AppServices() (composition root)
→ _services.ThemeService.Application = this
→ _ = InitializeServicesAsync() (async, sans bloquer l'UI)
→ crée MainWindow (desktop) / MainView (mobile) avec DataContext = RootViewModel
AppServices.cs construit à la main : la base SQLite, les repositories locaux,
le SyncEngine, le MobileApiClient, la navigation, ShellViewModel, RootViewModel.
Puis InitializeServicesAsync() :
_services.InitializeAsync()→ schéma SQLite + thème ;_services.InitializeSessionAsync()→ restaure le token + charge le bootstrap ;_services.RootViewModel.EvaluateSessionAsync()→ décide Activation ou Shell.
Où chercher en cas de bug de démarrage :
- Écran vide / crash au lancement →
App.axaml.cs,AppServices.cs. - L'app reste sur l'écran d'activation →
RootViewModel.EvaluateSessionAsync(),MobileSessionService.HasValidSessionAsync(). - Erreur SQLite →
LocalDatabase(schéma),AppServices.InitializeAsync().
8. Flux 2 : activation de licence
Côté mobile (déclencheur) : MobileSessionService.ActivateAsync(codeClient, numeroLicence).
1. MobileSessionService.ActivateAsync
2. → MobileApiClient.ActivateAsync (POST /api/license/activate, JSON)
3. → LicenseService.ActivateAsync (côté serveur, Omika.Core)
4. → LicenceRepository.GetClientByCodeAsync (HFSQL: ClientBase)
5. → LicenceRepository.GetByNumeroAndClientAsync (HFSQL: Licence)
6. → vérifie : active ? non expirée ? non attribuée à un autre mobile ?
7. → si OK : AssignDeviceAsync (LI_IDMobile) + AddHistoAsync (LicenceHisto)
8. → renvoie LicenseResponse { IsValid, AccessToken (si valide), ... }
9. → si valide : PersistSessionAsync (token en secure storage + bootstrap en SQLite)
10. → RootViewModel.OnActivationSucceeded() → affiche le Shell
Règles métier (dans LicenseService.ActivateAsync), dans l'ordre :
| # | Vérification | Code d'erreur si échec |
|---|---|---|
| 1 | Données présentes (codeClient, numéro, deviceId) | E1001 (DataEmpty) |
| 2 | Client existe | E2001 (ClientInconnu) |
| 3 | Licence existe et appartient au client | E3001 (LicenceInconnue) |
| 4 | Licence active | E3002 (LicenceInactive) |
| 5 | Licence non expirée | E3003 (LicenceExpiree) |
| 6 | DeviceId non attribué à un autre mobile | E3004 (LicenceAttribuee) |
Où chercher en cas de bug d'activation :
- "Client inconnu" / "Licence inconnue" →
LicenceRepository+ la base Licence HFSQL (noms de base/table réels !). - "Licence inactive/expirée" → données HFSQL (
LI_Active,LI_DateExpiration). - "Licence attribuée à un autre mobile" →
LI_IDMobiledéjà renseigné. - Le token ne revient pas →
LicenseEndpoints.BuildSession+TokenService(cléMobileAuth:SigningKey).
9. Flux 3 : synchronisation mobile → serveur
C'est le flux le plus important. Il y a deux acteurs : le mobile (SyncEngine)
et le serveur (Omika.Api + Omika.Worker).
9.1 Côté mobile — SyncEngine.SyncAsync()
1. Vérifie le réseau (NetworkConnectivity.IsOnlineAsync)
2. Lit l'Outbox (OutboxRepository.GetReadyAsync)
3. Pour chaque opération prête :
- diffère si une dépendance n'est pas encore réconciliée
- envoie : SendOperationAsync (POST /api/sync/operations)
OU SendDocumentAsync (POST /api/documents) pour les documents
4. Interroge les statuts (GetStatusAsync pour chaque OperationId)
5. Réconcilie les identités (MobileIdentityReconciliationService.ReconcileAsync)
6. Applique le snapshot (SnapshotApplier.ApplyAsync)
Protégé par un SemaphoreSlim (un seul cycle à la fois). Déclenché par
ShellViewModel.SyncNowAsync (bouton de sync).
9.2 Côté API — SyncOperationService.ReceiveAsync()
1. Valide OperationId, DeviceId, EntityType, OperationType, EntityGuid, Payload
2. Vérifie l'idempotence (opération déjà reçue ?)
3. Construit le SyncEnvelope (identités depuis le token, jamais du payload)
4. InsertToSrvAsync → écrit dans TOSRV
5. WriteLogAsync → écrit un état "Pending" dans MobLog
6. Retourne "RECEIVED"
9.3 Côté Worker — SyncWorker → SyncProcessor.ProcessAsync()
Phase 1 : TransferToReceptionAsync
TOSRV → MobReception (copie), puis suppression TOSRV
Phase 2 : GetPendingOperationsAsync
lit MobReception (MR_Fait = false), exclut les opérations terminales
Phase 3 : trie les opérations (CREATE → UPDATE → ARCHIVE/DELETE, puis par date)
Phase 4 : pour chaque opération :
handler = HandlerRegistry.GetHandler(EntityType)
résultat = handler.ApplyAsync(opération)
→ Success : MarkSuccessAsync (supprime MobReception + écrit MobLog SUCCESS)
→ TransientFailure : IncrementRetryAsync (retry, max 30)
→ PermanentFailure : MarkRejectedAsync (écrit MobLog REJECTED)
9.4 Les handlers
| Handler | EntityType | Actions supportées |
|---|---|---|
InterventionSyncOperationHandler |
Intervention |
CREATE, UPDATE, ARCHIVE. DELETE → échec permanent. |
SuiviTempsSyncOperationHandler |
SuiviTemps |
(création/suivi des temps) |
DefaultSyncOperationHandler |
tout le reste | Refuse proprement (NO_HANDLER) |
9.5 L'idempotence et le retry (anti-bug essentiel)
- Chaque opération a un
OperationId(GUID) unique généré par le mobile. - Si la même opération est rejouée (réseau coupé, retry), le serveur la reconnaît et ne la duplique pas.
- Retry :
RetryCountincrémenté à chaque échec transitoire (TRANSIENT_FAILURE). Au-delà deMaxRetryCount(défaut 30), l'opération passe enREJECTED(terminal). - Un échec définitif (
PERMANENT_FAILURE) est rejeté immédiatement (ex. DELETE non supporté).
Où chercher en cas de bug de sync :
- "L'opération reste en attente" →
TOSRV/MobReception(le Worker n'a pas tourné ?). - "L'opération est rejetée" →
MobLog.ML_Message(code d'erreur + message). - "Rien n'est envoyé" →
OutboxRepository,SyncEngine(hors-ligne ?),NetworkConnectivity. - "Double application" → vérifier l'idempotence par
OperationId.
10. Flux 4 : snapshot serveur → mobile
Quand le mobile se synchronise, il récupère une "photo" complète des données :
1. MobileApiClient.GetSnapshotAsync (GET /api/sync/snapshot, authentifié)
2. → MobileSnapshotService.GetSnapshotAsync (côté serveur)
- charge Clients, Collaborateurs, Sites, Interventions, Dossiers,
RendezVous (filtrés par collaborateur), Documents, SuiviTemps
- résout les identités publiques (GUID) via MobileEntityIdentityResolver
3. → renvoie MobileSnapshotResponse
4. SnapshotApplier.ApplyAsync (côté mobile)
- validation du snapshot (ex. Id unique des Personnes)
- transaction SQLite : DELETE + INSERT de chaque table métier
- commit atomique (en cas d'erreur : rollback, l'ancienne base reste utilisable)
Point important (bug fréquent) : le snapshot ne touche jamais les tables
techniques (Outbox, IdentityMapping, SyncState, DocumentCache, LocalBootstrap).
Où chercher en cas de bug de snapshot :
- "Snapshot timeout" →
MobileSnapshotService(les docs mentionnent un ancien problème de N+1 résolu par du chargement BULK — voirLoadInterventionsAsync). - "Snapshot non appliqué" →
SnapshotApplier+ logs (REFRESH FAILED stage=...). - "Données locales vides" → le filtre par collaborateur (
FilterRendezVousByCollaborateur).
11. Flux 5 : documents
Upload (mobile → serveur)
SyncEngine.SendDocumentAsync
→ lit le fichier local référencé dans le payload
→ MobileApiClient.UploadDocumentAsync (POST /api/documents)
→ MobileDocumentService.UploadAsync (serveur)
→ IDocumentFileStorage.SanitizeFileName / Write (stockage physique)
→ IDocumentRepository.CreateAsync (métadonnées en base)
→ renvoie DocumentUploadResponse
Download (serveur → mobile)
MobileApiClient.DownloadDocumentAsync (GET /api/documents/{guid})
→ MobileDocumentService.DownloadAsync
→ résout DocumentGuid → identité physique
→ IDocumentRepository.GetByPhysicalIdentityAsync
→ IDocumentFileStorage.Read (contenu binaire)
→ renvoie DocumentDownloadResponse
Règles de sécurité : le DocumentGuid est l'identifiant public ; le PK HFSQL et
le chemin physique du serveur ne sont jamais exposés au mobile.
Où chercher en cas de bug document :
- "Fichier local introuvable" →
SyncEngine.SendDocumentAsync(étatRejected). - "Nom de fichier invalide" →
IDocumentFileStorage.SanitizeFileName. - "Taille dépassée" →
IDocumentFileStorage.Write(exceptionDOCUMENT_SIZE_EXCEEDED). - "Document introuvable" →
MobileDocumentService.DownloadAsync(résolution d'identité).
12. L'authentification par token
- Émission : après une licence valide,
TokenService.GenerateToken(identity)crée un jeton stateless signé en HMAC-SHA256 :payloadBase64.signature. - Validation :
MobileAuthMiddlewarelit le headerAuthorization: Bearer <token>, appelleTokenService.ValidateToken, et stocke l'identité dansHttpContext.Items(cléOmika.MobileIdentity). Les endpoints la récupèrent viaGetMobileIdentity(). - Durée de vie :
MobileAuth:TokenLifetimeHours(config). - Clé de signature :
MobileAuth:SigningKey, obligatoire en Production (leProgram.csde l'API lève une erreur au démarrage si elle manque).
Où chercher en cas de bug d'auth :
- "401 Unauthorized" sur un endpoint → token absent/expiré/invalide →
TokenService.ValidateToken. - "Clé de signature manquante" au démarrage → config
MobileAuth:SigningKey.
13. La configuration
13.1 src/Omika.Api/appsettings.json
Omika:DatabaseVersion→Auto/Batik/OmikaLicenseConnection→ serveur/port/base de la base LicenceSyncConnection→ serveur/port/base Sync (obsolète)Logging,AllowedHosts
13.2 src/Omika.Worker/appsettings.json
Omika:DatabaseVersionHfsql→ connexion Business (serveur, port, base, login, driver)SyncConnection→ base Sync (obsolète)Sync→MaxRetryCount(30),RetryDelaySeconds(120),LogRetentionDays(30)Logging
13.3 Config locale mobile
MobileApiClient.BaseUrl(défauthttp://localhost:5000), surchargeable viamobile-api.local.json(fichier local, non sensible).- Le token est stocké en secure storage (jamais en clair dans SQLite).
Les secrets (
Password,FilePasswords,SigningKey) doivent venir de configuration sécurisée (variables d'environnement / User Secrets), jamais en clair.
14. Les codes d'erreur et états
14.1 Licence (LicenseErrorCodes)
| Code | Signification |
|---|---|
E0000 |
OK |
E1001 |
Données vides |
E1002 |
Format JSON invalide |
E2001 |
Client inconnu |
E2002 |
Erreur licence client |
E2003 |
Configuration incomplète |
E2004 |
Clé API incorrecte |
E3001 |
Licence inconnue |
E3002 |
Licence inactive |
E3003 |
Licence expirée |
E3004 |
Licence attribuée à un autre mobile |
E3005 |
Aucune licence |
14.2 Synchronisation
SyncStatus:Success,Rejected,Pending,StatusNotAvailable.SyncOperationState:Unknown,Pending,Success,Rejected.SyncOperationType:Create,Update,Archive,Delete.- Résultat de handler :
Success,TransientFailure(→ retry),PermanentFailure(→ rejet).
Codes d'erreur de handler (exemples) :
INTERVENTION_INVALID_ENTITY_GUID—EntityGuidvide ;INTERVENTION_OPERATION_NOT_SUPPORTED— type d'opération non supporté (ex. DELETE) ;INTERVENTION_NOT_FOUND— entité introuvable (transitoire si un CREATE est en attente) ;INTERVENTION_VALIDATION_FAILED— échec de validation métier ;NO_HANDLER— aucun handler pour cetEntityType.
Codes d'erreur documents :
DOCUMENT_GUID_INVALID,DOCUMENT_FILENAME_INVALID,DOCUMENT_SIZE_EXCEEDED,DOCUMENT_UPLOAD_FAILED.
15. Guide de diagnostic
Chaque ligne : symptôme → où regarder en premier.
Démarrage / interface
- L'app ne démarre pas →
Omika.App.Desktop/Program.cs,App.axaml.cs. - Écran vide / "Not Found: ...ViewModel" →
ViewLocator.cs(le lien Vue↔ViewModel est cassé). - Mauvaise page affichée au départ →
RootViewModel(Activation vs Shell). - Navigation qui ne répond pas →
NavigationService.cs,ShellViewModel.OnPageChanged. - Bouton de sync qui ne fait rien →
ShellViewModel.SyncNowAsync,SyncEngine.
Session / licence
- L'app reste sur l'écran d'activation →
MobileSessionService.HasValidSessionAsync,RootViewModel.EvaluateSessionAsync. - "Client inconnu" →
LicenceRepository.GetClientByCodeAsync+ base Licence réelle. - "Licence inconnue" →
LicenceRepository.GetByNumeroAndClientAsync+ base Licence. - "Licence inactive/expirée" → données HFSQL (
LI_Active,LI_DateExpiration). - "Attribuée à un autre mobile" →
LI_IDMobile. - Token absent →
TokenService.GenerateToken+MobileAuth:SigningKey.
Synchronisation
- "Hors ligne" affiché →
NetworkConnectivity.IsOnlineAsync. - Rien n'est envoyé →
OutboxRepository.GetReadyAsync,SyncEngine.SyncAsync(étape 2). - Opération "différée" → dépendance non réconciliée (
SyncEngine,MobileIdentityReconciliationService). - Opération rejetée → lire
MobLog.ML_Message(code + message), puis le handler correspondant. - Opération qui boucle en retry →
SyncProcessor.RetryAsync,MaxRetryCount. - Données locales fausses →
SnapshotApplier,MobileSnapshotService. - Doublons → idempotence
OperationId(SyncOperationService.ReceiveAsync,SyncProcessor).
Base de données
- Erreur de connexion HFSQL →
src/Omika.Data/Connection/,HfsqlOptions, configHfsql/LicenseConnection/ServiceConnection. - "Table/colonne introuvable" → divergence entre le modèle attendu et la base réelle (
HfSql/Models,SchemaDetection). - Erreur SQLite →
LocalDatabase,SnapshotApplier,MobileIdentityReconciliationService.
16. Les points de trace / logs
- Console mobile :
Console.WriteLine("REFRESH START"),"REFRESH SUCCESS/Failed stage=...","SQLITE IMPORT END"(dansSyncEngine,SnapshotApplier). - Logs serveur :
ILogger(.LogInformation/.LogWarning/.LogError) dansSyncEngine,SyncWorker,SyncProcessor,MobileSessionService, etc. - API : logs
SNAPSHOT START/ENDdansSyncEndpoints. MobLog: le journal terminal des opérations de sync (succès / rejet) — c'est le premier endroit à consulter pour comprendre pourquoi une opération a été rejetée.
17. Les pièges et limites connus
- Divergence de topologie HFSQL : les docs indiquent que certaines bases/tables
attendues ne sont pas toutes présentes sur le serveur réel (ex.
BASE_LICENCESsans les tablesLicence/ClientBase/LicenceHisto;SYNC_TESTvsOMIKA_TEST). → Toujours vérifier la base réelle avant de conclure à un bug de code. - Un seul Worker actif par file Sync (pas de verrou distribué). Deux Workers simultanés peuvent poser problème.
- Support Windows Service : prévu (
UseWindowsService()) mais pas encore activé. - Handlers limités : seuls
InterventionetSuiviTempssont branchés ; les autresEntityTypepassent par le fallbackNO_HANDLER(rejet). - DELETE non supporté : la suppression physique est refusée ; on utilise
ARCHIVE. - Le desktop accède à HFSQL directement, le mobile uniquement via l'API : ne pas confondre les deux chemins.
- DI : le dernier enregistrement gagne (
AddOmikaDataBusinessOleDbdoit être appelé aprèsAddOmikaData). - Le token n'est jamais stocké en SQLite en clair (secure storage). Ne pas chercher le token dans la base locale.
Annexe — arborescence rapide des fichiers les plus importants
src/
Omika.Core/
Services/Mobile/ ← serveur : licence, token, sync, snapshot, bootstrap, documents
Domain/ ← entités + objets-valeur + exceptions
Contracts/ ← DTO
Interfaces/ ← contrats
Runtime/ ← sélection Batik/Omika
Omika.Data/
ServiceCollectionExtensions.cs ← le branchement DI des connexions
Repositories/Technical/ ← LicenceRepository, SyncTransferRepository
Repositories/Adaptive*.cs ← repositories Batik/Omika
Connection/ ← connexions HFSQL (ODBC/OLE DB)
Omika.Api/
Program.cs ← point d'entrée + DI + endpoints
Endpoints/ ← tous les endpoints
MobileAuthMiddleware.cs ← validation du Bearer token
Omika.Worker/
Program.cs ← point d'entrée + DI
SyncWorker.cs ← la boucle
SyncProcessor.cs ← la logique de traitement
*SyncOperationHandler.cs ← les handlers métier
Omika.App.Local/
Services/SyncEngine.cs ← le moteur de sync mobile
Services/MobileApiClient.cs ← le client HTTP
Services/MobileSessionService.cs ← la session
Services/SnapshotApplier.cs ← application du snapshot
Sqlite/ ← base locale
Omika.App/
App.axaml.cs ← point d'entrée Avalonia
AppServices.cs ← composition root
Views/ + ViewModels/ ← UI + logique (MVVM)
Navigation/ ← navigation
tests/Omika.Tests/ ← les tests
tools/ ← sondes de diagnostic
ANNEXE B — Tracer un bug de synchronisation de bout en bout
Cette annexe est un mode d'emploi pratique. Elle reprend le flux de synchronisation et indique, pour chaque étape, le fichier exact, la méthode exacte, et ce qu'il faut vérifier quand ça ne marche pas.
B.1 Le diagramme de séquence complet (aller-retour)
TÉLÉPHONE (mobile) SERVEUR
──────────────────────────── ────────────────────────────────────────
ShellViewModel.SyncNowAsync
└─ SyncEngine.SyncAsync
│ 1. NetworkConnectivity.IsOnlineAsync
│ 2. OutboxRepository.GetReadyAsync
│
│ 3. pour chaque opération :
│ MobileApiClient.SendOperationAsync
│─────────────────────────────► POST /api/sync/operations
│ SyncOperationService.ReceiveAsync
│ 1. valide (OperationId, DeviceId, …)
│ 2. idempotence (déjà reçu ?)
│ 3. SyncTransferRepository.InsertToSrvAsync
│ → INSERT dans TOSRV (base Service)
│ 4. SyncTransferRepository.WriteLogAsync
│ → INSERT MobLog (Pending)
│◄───────────────────────────── { "status": "RECEIVED" }
│
│ 4. MobileApiClient.GetStatusAsync
│─────────────────────────────► GET /api/sync/status/{operationId}
│◄───────────────────────────── SyncStatus (Pending / Success / Rejected)
│
│ 5. MobileIdentityReconciliationService.ReconcileAsync
│ (si le serveur a retourné un GUID canonique différent)
│
│ 6. MobileApiClient.GetSnapshotAsync
│─────────────────────────────► GET /api/sync/snapshot
│ MobileSnapshotService.GetSnapshotAsync
│◄───────────────────────────── MobileSnapshotResponse
│ SnapshotApplier.ApplyAsync
│ (DELETE + INSERT de chaque table métier SQLite, transaction)
▼
====== EN PARALLÈLE (Worker, en boucle) ======
SyncWorker.ExecuteAsync
└─ SyncProcessor.ProcessAsync
Phase 1 : TransferToReceptionAsync
TOSRV → MobReception (copie)
puis DELETE TOSRV
Phase 2 : GetPendingOperationsAsync
lit MobReception (MR_Fait = false)
Phase 3 : trie CREATE → UPDATE → ARCHIVE
Phase 4 : ProcessOperationAsync
→ handler.ApplyAsync
→ Success / Retry / Reject
→ MobLog (terminal)
B.2 Le traçage étape par étape
Pour chaque étape : le fichier → la méthode → ce qu'il faut vérifier.
| # | Étape | Fichier → méthode | Vérifier si bug |
|---|---|---|---|
| 1 | Vérif réseau | App.Local/Services/NetworkConnectivity.cs → IsOnlineAsync |
L'app dit "Hors ligne" à tort → ici. |
| 2 | Lire l'Outbox | App.Local/Repositories/OutboxRepository.cs → GetReadyAsync |
"Rien n'est envoyé" → l'opération est-elle bien dans la table SQLite Outbox ? |
| 3 | Différer dépendance | SyncEngine.cs → bloc operation.DependencyEntityType |
Opération "différée" → dépendance non réconciliée. |
| 4 | Envoi HTTP | App.Local/Services/MobileApiClient.cs → SendOperationAsync |
Erreur 401/500 → token, BaseUrl, API démarrée ? |
| 5 | Réception API | Core/Services/Mobile/SyncOperationService.cs → ReceiveAsync |
Validation (OperationId/DeviceId/EntityType/Payload) → code 400. |
| 6 | Écriture TOSRV | Data/Repositories/Technical/SyncTransferRepository.cs → InsertToSrvAsync |
Connexion Service (OLE DB) OK ? |
| 7 | Transfert | SyncTransferRepository.cs → TransferToReceptionAsync |
TOSRV lu, copié dans MobReception, puis supprimé. |
| 8 | Lecture en attente | SyncTransferRepository.cs → GetPendingOperationsAsync |
MobReception avec MR_Fait = false. |
| 9 | Tri | Worker/SyncProcessor.cs → OrderOf + OrderBy |
CREATE avant UPDATE avant ARCHIVE. |
| 10 | Application | Worker/SyncProcessor.cs → ProcessOperationAsync |
Décision Success / Retry / Reject (voir B.4). |
| 11 | Handler | Worker/InterventionSyncOperationHandler.cs → ApplyAsync |
Logique métier Intervention (voir B.5). |
| 12 | Résultat | SyncTransferRepository.cs → MarkSuccessAsync / MarkRejectedAsync / IncrementRetryAsync |
MobLog terminal. |
B.3 Exemple concret : "mon intervention créée sur le mobile n'arrive jamais"
Suivre cette checklist dans l'ordre :
-
Le mobile a-t-il bien mis l'opération en Outbox ? → Regarder la table SQLite
Outbox(omika.db). Si vide, le problème est en amont (le service local qui crée l'intervention n'a pas appeléOutboxService.EnqueueAsync). -
L'opération part-elle ? →
SyncEngine.SyncAsync: vérifier les logsREFRESH START/REFRESH FAILED stage=.... Sistageindique une exception, lireresult.FailureStageetresult.ExceptionType. -
L'API a-t-elle reçu ? → Regarder les logs de
Omika.Api(ou un outil commeMobLog). SiRECEIVEDest retourné, l'opération est dansTOSRV. -
Le Worker l'a-t-il traitée ? → Regarder
TOSRV(doit se vider) puisMobReception(doit contenir l'opération avecMR_Fait = false), puisMobLog(résultat terminal). SiTOSRVreste plein → le Worker ne tourne pas ou la connexion Service est cassée. SiMobReceptionreste plein → le handler échoue (voirMobLog.ML_Message). -
Le résultat est-il revenu au mobile ? → Le mobile interroge
/api/sync/status/{operationId}. SiRejected, lire le code d'erreur dansMobLog.ML_Messageet le corréler avec la section B.5.
B.4 L'arbre de décision de ProcessOperationAsync (le cœur du Worker)
Worker/SyncProcessor.cs → ProcessOperationAsync :
1. RetryCount >= MaxRetryCount (30) ?
OUI → RejectAsync("MAX_RETRY") → REJECTED (terminal)
NON → continuer
2. Idempotence : GetTerminalResultAsync(OperationId) existe déjà ?
OUI + Success → reconstruit SUCCESS sans réappliquer le handler
OUI + Rejected → reconstruit REJECTED sans réappliquer le handler
NON → continuer
3. handler = HandlerRegistry.GetHandler(EntityType) ?? DefaultSyncOperationHandler
4. Ouvre UNE transaction OLE DB locale (Business/WebService)
5. result = handler.ApplyAsync(operation)
Success → MarkSuccessAsync + Commit → SUCCESS
PermanentFailure → Rollback + MarkRejectedAsync → REJECTED
TransientFailure → Rollback + IncrementRetryAsync → RETRY (retryCount + 1)
Exception → Rollback + IncrementRetryAsync → RETRY
À retenir pour le debug :
TRANSIENT_FAILURE= erreur temporaire → retry (ex. un CREATE pas encore arrivé).PERMANENT_FAILURE= erreur définitive → rejet immédiat (ex. DELETE non supporté, validation métier).- L'idempotence empêche de réappliquer deux fois le même
OperationId.
B.5 Le handler Intervention en détail (exemple à connaître)
Worker/InterventionSyncOperationHandler.cs — ApplyAsync :
EntityGuid vide ? → PERMANENT_FAILURE ("INTERVENTION_INVALID_ENTITY_GUID")
OperationType :
Create → HandleCreateAsync
Update → HandleUpdateAsync
Archive → HandleArchiveAsync
Delete → PERMANENT_FAILURE ("INTERVENTION_OPERATION_NOT_SUPPORTED")
(la suppression physique est interdite → utiliser ARCHIVE)
CREATE (HandleCreateAsync)
- Désérialise le payload (
InterventionSyncPayload). Si invalide →INTERVENTION_INVALID_PAYLOAD. - Appelle
IInterventionService.CreateAsync. - Si succès →
Success(created.Id, requestedEntityGuid: operation.EntityGuid, canonicalEntityGuid: ...). - Si
DomainValidationException→PERMANENT_FAILURE ("INTERVENTION_VALIDATION_FAILED").
UPDATE (HandleUpdateAsync)
- Désérialise le payload. Si invalide →
INTERVENTION_INVALID_PAYLOAD. - Résout l'identité physique (
ResolvePhysicalIdentity). Si non résolvable →TRANSIENT_FAILURE ("INTERVENTION_NOT_FOUND")— un CREATE peut encore être en attente. - Vérifie l'existence. Si absente →
TRANSIENT_FAILURE(retry). - Sinon →
IInterventionService.UpdateAsync.
ARCHIVE (HandleArchiveAsync)
- Résout l'identité physique. Si non résolvable →
TRANSIENT_FAILURE. - Vérifie l'existence. Si absente →
TRANSIENT_FAILURE. - Sinon →
IInterventionService.ArchiveAsync.
Le fallback (DefaultSyncOperationHandler)
- Ne déclare aucun
EntityType. S'il est sélectionné, c'est qu'aucun handler n'existe pour ce type →PERMANENT_FAILURE ("NO_HANDLER"). Jamais de faux SUCCESS.
B.6 Récapitulatif "où est le problème ?" en une phrase
| Où l'opération est bloquée | Cause probable | Fichier à ouvrir |
|---|---|---|
| Jamais dans l'Outbox | Le service local n'a pas enregistré le changement | App.Local/Services/*LocalService.cs, OutboxService.cs |
| Dans l'Outbox mais jamais envoyée | Hors ligne, ou SyncEngine en erreur |
NetworkConnectivity.cs, SyncEngine.cs |
| Envoyée mais rejetée par l'API (400) | Validation échouée (OperationId/DeviceId/Payload) | SyncOperationService.ReceiveAsync |
Dans TOSRV mais pas traitée |
Worker arrêté, ou connexion Service cassée | SyncWorker.cs, SyncTransferRepository.cs |
Dans MobReception mais jamais finalisée |
Handler échoue (voir MobLog) |
*SyncOperationHandler.cs, SyncProcessor.cs |
MobLog = REJECTED |
Erreur métier/définitive | MobLog.ML_Message → corréler avec B.5 |
ANNEXE C — Tracer un bug d'activation de licence
C.1 Le diagramme de séquence
TÉLÉPHONE (mobile) SERVEUR
──────────────────────────── ──────────────────────────────
LicenseActivationViewModel
└─ MobileSessionService.ActivateAsync(codeClient, numeroLicence)
│ 1. lit deviceId (IDeviceIdProvider)
│ 2. MobileApiClient.ActivateAsync
│─────────────────────────────► POST /api/license/activate
│ LicenseService.ActivateAsync
│ 1. LicenceRepository.GetClientByCodeAsync
│ → SELECT ClientBase (HFSQL)
│ 2. LicenceRepository.GetByNumeroAndClientAsync
│ → SELECT Licence (HFSQL)
│ 3. vérifie active / expiration / deviceId
│ 4. si OK : AssignDeviceAsync (UPDATE Licence)
│ + AddHistoAsync (INSERT LicenceHisto)
│ LicenseEndpoints.BuildSession
│ → TokenService.GenerateToken (si valide)
│◄───────────────────────────── LicenseResponse + AccessToken
│ 3. si valide : PersistSessionAsync
│ → token en secure storage (ISecureTokenStore)
│ → GET /api/mobile/bootstrap → sauvegardé en SQLite
▼
RootViewModel.OnActivationSucceeded → affiche le Shell
C.2 La checklist "l'activation échoue"
Suivre dans l'ordre :
-
Le mobile appelle-t-il l'API ? →
MobileSessionService.ActivateAsync. VérifierBaseUrl(défauthttp://localhost:5000) et que l'API tourne. -
Quel code d'erreur est renvoyé ? →
LicenseResponse.ErrorCode+ErrorMessage. Corréler avec le tableau C.3. -
Le client existe-t-il en base ? →
LicenceRepository.GetClientByCodeAsync→ tableClientBase(CL_Code). -
La licence existe-t-elle pour ce client ? →
LicenceRepository.GetByNumeroAndClientAsync→ tableLicence(LI_Numero+LI_IDClient). -
La licence est-elle active / non expirée / non déjà attribuée ? → colonnes
LI_Active,LI_DateExpiration,LI_IDMobile. -
Le token est-il bien émis ? →
LicenseEndpoints.BuildSession+TokenService.GenerateToken(cléMobileAuth:SigningKeyobligatoire).
C.3 Correspondance code d'erreur → cause → fichier
| Code | Cause | Où regarder |
|---|---|---|
E1001 |
Données incomplètes (codeClient, numéro, deviceId) | LicenseService.ActivateAsync (étape 1) |
E2001 |
Client inconnu | LicenceRepository.GetClientByCodeAsync + table ClientBase |
E3001 |
Licence inconnue | LicenceRepository.GetByNumeroAndClientAsync + table Licence |
E3002 |
Licence inactive | LI_Active |
E3003 |
Licence expirée | LI_DateExpiration |
E3004 |
Licence attribuée à un autre mobile | LI_IDMobile |
⚠️ Rappel (déjà signalé dans les docs) : la base Licence réelle (
BASE_LICENCES) peut ne pas contenir les tablesClientBase/Licence/LicenceHisto. Vérifier la base réelle avant de conclure à un bug de code.
ANNEXE D — Tracer un bug de document (upload / download)
D.1 Upload (mobile → serveur)
SyncEngine.SendDocumentAsync
│ 1. désérialise le payload (DocumentOutboxPayload)
│ 2. vérifie File.Exists(payload.LocalPath) → sinon Rejected
│ 3. lit le fichier en bytes
│ 4. MobileApiClient.UploadDocumentAsync
│─────────────────────────────► POST /api/documents
│ MobileDocumentService.UploadAsync
│ → SanitizeFileName (nom sûr ?)
│ → IDocumentFileStorage.Write (stockage)
│ → IDocumentRepository.CreateAsync (métadonnées)
│◄───────────────────────────── DocumentUploadResponse
│ 5. si Success → OutboxState.Accepted
│ sinon → OutboxState.Rejected + LastError
Checklist upload :
- "Fichier local introuvable" →
SyncEngine.SendDocumentAsync(étape 2). - "Nom de fichier invalide" →
MobileDocumentService.UploadAsync(SanitizeFileName). - "Taille dépassée" →
IDocumentFileStorage.Write(DOCUMENT_SIZE_EXCEEDED). - "Upload échoué" →
IDocumentRepository.CreateAsync(DOCUMENT_UPLOAD_FAILED).
D.2 Download (serveur → mobile)
MobileApiClient.DownloadDocumentAsync
│─────────────────────────────► GET /api/documents/{documentGuid}
│ MobileDocumentService.DownloadAsync
│ → résout DocumentGuid → identité physique
│ → IDocumentRepository.GetByPhysicalIdentityAsync
│ → IDocumentFileStorage.Read (bytes)
│◄───────────────────────────── DocumentDownloadResponse
Checklist download :
- "Document introuvable" → résolution d'identité (
ResolvePhysicalIdentity) ouGetByPhysicalIdentityAsyncretournenull. - Contenu vide →
IDocumentFileStorage.Read(chemin physique invalide ?).
ANNEXE E — Lancer l'API et le Worker en local (guide pratique)
E.1 Prérequis
- Un serveur HFSQL doit être joignable à
127.0.0.1:4900(config de test). - Le fichier
config/hfsql-test.local.jsonest chargé uniquement en Development/Test (viaHfSqlTestConfigurationLoader.AddLocalTestConfiguration). Il contient les connexionsHfsql(Business),LicenseConnection,SyncConnection,ServiceConnection, la cléMobileAuth:SigningKeyet les flagsE2E. - ⚠️ Point de sécurité important : ce fichier de test contient
"AllowHfSqlWrites": false→ les écritures HFSQL sont désactivées dans ce mode. C'est un environnement de lecture seule pour déboguer sans risque. - L'API et le Worker font un fail-fast au démarrage : ils appellent
IDatabaseRuntimeInitializer.InitializeAsync()et lèvent une exception si la base est injoignable ou inconnue. Sans HFSQL démarré, ils ne démarrent pas.
E.2 Commandes PowerShell (depuis D:\Avalonia\Omika)
# 1. Restaurer les packages
dotnet restore .\Omika.slnx
# 2. Compiler
dotnet build .\Omika.slnx
# 3a. Lancer l'API (ou utiliser le script tout prêt)
dotnet run --project .\src\Omika.Api\Omika.Api.csproj
# 3b. Lancer le Worker (ou utiliser le script tout prêt)
dotnet run --project .\src\Omika.Worker\Omika.Worker.csproj
Il existe aussi des scripts qui démarrent en arrière-plan et redirigent les logs :
.\tools\start_api_e2e.ps1 # → logs dans api_e2e_out.txt / api_e2e_err.txt
.\tools\start_worker_e2e.ps1 # → logs dans worker_e2e_out.txt / worker_e2e_err.txt
Pour l'application mobile/desktop, l'API doit être joignable à l'URL configurée
(défaut http://localhost:5000, surchargeable via mobile-api.local.json).
E.3 Les logs à observer
| Composant | Où regarder |
|---|---|
| API | Console / api_e2e_out.txt, api_e2e_err.txt. Endpoint /health. |
| Worker | Console / worker_e2e_out.txt, worker_e2e_err.txt. Logs "Worker de synchronisation démarré". |
| Mobile | Console.WriteLine : REFRESH START, REFRESH SUCCESS/FAILED stage=..., SQLITE IMPORT END. |
| Sync serveur | MobLog (résultats terminaux) + logs ILogger dans SyncProcessor/SyncWorker. |
Fin du guide. Document généré en lecture seule du code source ; aucun fichier source n'a été modifié.