# 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 1. [Le vocabulaire indispensable](#1-le-vocabulaire-indispensable) 2. [Vue d'ensemble en une image](#2-vue-densemble-en-une-image) 3. [Les deux générations (legacy WINDEV → moderne .NET)](#3-les-deux-générations) 4. [La carte des 13 projets](#4-la-carte-des-13-projets) 5. [Le graphe des dépendances](#5-le-graphe-des-dépendances) 6. [Les bases de données et connexions](#6-les-bases-de-données-et-connexions) 7. [Flux 1 : démarrage de l'application](#7-flux-1--démarrage-de-lapplication) 8. [Flux 2 : activation de licence](#8-flux-2--activation-de-licence) 9. [Flux 3 : synchronisation mobile → serveur](#9-flux-3--synchronisation-mobile--serveur) 10. [Flux 4 : snapshot serveur → mobile](#10-flux-4--snapshot-serveur--mobile) 11. [Flux 5 : documents (upload / download)](#11-flux-5--documents) 12. [L'authentification par token](#12-lauthentification-par-token) 13. [La configuration (où sont les réglages)](#13-la-configuration) 14. [Les codes d'erreur et états](#14-les-codes-derreur-et-états) 15. [Guide de diagnostic : symptôme → où chercher](#15-guide-de-diagnostic) 16. [Les points de trace / logs](#16-les-points-de-trace--logs) 17. [Les pièges et limites connus](#17-les-pièges-et-limites-connus) --- ## 1. Le vocabulaire indispensable - **Solution (`.slnx`)** : le conteneur qui regroupe tous les projets. Ici `Omika.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 `OperationId` unique). --- ## 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.Api` **reçoit** les opérations ; `Omika.Worker` les **applique**. - `Omika.Core` est le cerveau métier ; `Omika.Data` est 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/validate` - `MobileBootstrapEndpoints` → `GET /api/mobile/bootstrap` - `SyncEndpoints` → `POST /api/sync/operations`, `GET /api/sync/status/{id}`, `GET /api/sync/snapshot` - `MobileDocumentEndpoints` → `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`, `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.Core` ne dépend de **personne** (c'est la base). - `Omika.Data` ne dépend que de `Omika.Core`. - `Omika.Api` et `Omika.Worker` partagent `Core` + `Data`. - L'UI (`App`) dépend de `Core` + `App.Local`, **jamais** de `Data` (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ès** `AddOmikaData()`. --- ## 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() .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()` : 1. `_services.InitializeAsync()` → schéma SQLite + thème ; 2. `_services.InitializeSessionAsync()` → restaure le token + charge le bootstrap ; 3. `_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_IDMobile` dé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** : `RetryCount` incrémenté à chaque échec transitoire (`TRANSIENT_FAILURE`). Au-delà de `MaxRetryCount` (défaut **30**), l'opération passe en `REJECTED` (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 — voir `LoadInterventionsAsync`). - "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` (état `Rejected`). - "Nom de fichier invalide" → `IDocumentFileStorage.SanitizeFileName`. - "Taille dépassée" → `IDocumentFileStorage.Write` (exception `DOCUMENT_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** : `MobileAuthMiddleware` lit le header `Authorization: Bearer `, appelle `TokenService.ValidateToken`, et stocke l'identité dans `HttpContext.Items` (clé `Omika.MobileIdentity`). Les endpoints la récupèrent via `GetMobileIdentity()`. - **Durée de vie** : `MobileAuth:TokenLifetimeHours` (config). - **Clé de signature** : `MobileAuth:SigningKey`, **obligatoire en Production** (le `Program.cs` de 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` / `Omika` - `LicenseConnection` → serveur/port/base de la base Licence - `SyncConnection` → serveur/port/base Sync (obsolète) - `Logging`, `AllowedHosts` ### 13.2 `src/Omika.Worker/appsettings.json` - `Omika:DatabaseVersion` - `Hfsql` → 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éfaut `http://localhost:5000`), surchargeable via `mobile-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` — `EntityGuid` vide ; - `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 cet `EntityType`. 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`, config `Hfsql`/`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"` (dans `SyncEngine`, `SnapshotApplier`). - **Logs serveur** : `ILogger` (`.LogInformation` / `.LogWarning` / `.LogError`) dans `SyncEngine`, `SyncWorker`, `SyncProcessor`, `MobileSessionService`, etc. - **API** : logs `SNAPSHOT START/END` dans `SyncEndpoints`. - **`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 1. **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_LICENCES` sans les tables `Licence`/`ClientBase`/`LicenceHisto` ; `SYNC_TEST` vs `OMIKA_TEST`). → Toujours vérifier la **base réelle** avant de conclure à un bug de code. 2. **Un seul Worker actif** par file Sync (pas de verrou distribué). Deux Workers simultanés peuvent poser problème. 3. **Support Windows Service** : prévu (`UseWindowsService()`) mais pas encore activé. 4. **Handlers limités** : seuls `Intervention` et `SuiviTemps` sont branchés ; les autres `EntityType` passent par le fallback `NO_HANDLER` (rejet). 5. **DELETE non supporté** : la suppression physique est refusée ; on utilise `ARCHIVE`. 6. **Le desktop accède à HFSQL directement**, le mobile uniquement via l'API : ne pas confondre les deux chemins. 7. **DI : le dernier enregistrement gagne** (`AddOmikaDataBusinessOleDb` doit être appelé après `AddOmikaData`). 8. **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** : 1. **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`). 2. **L'opération part-elle ?** → `SyncEngine.SyncAsync` : vérifier les logs `REFRESH START` / `REFRESH FAILED stage=...`. Si `stage` indique une exception, lire `result.FailureStage` et `result.ExceptionType`. 3. **L'API a-t-elle reçu ?** → Regarder les logs de `Omika.Api` (ou un outil comme `MobLog`). Si `RECEIVED` est retourné, l'opération est dans `TOSRV`. 4. **Le Worker l'a-t-il traitée ?** → Regarder `TOSRV` (doit se vider) puis `MobReception` (doit contenir l'opération avec `MR_Fait = false`), puis `MobLog` (résultat terminal). Si `TOSRV` reste plein → le Worker ne tourne pas ou la connexion **Service** est cassée. Si `MobReception` reste plein → le handler échoue (voir `MobLog.ML_Message`). 5. **Le résultat est-il revenu au mobile ?** → Le mobile interroge `/api/sync/status/{operationId}`. Si `Rejected`, lire le code d'erreur dans `MobLog.ML_Message` et 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 : 1. **Le mobile appelle-t-il l'API ?** → `MobileSessionService.ActivateAsync`. Vérifier `BaseUrl` (défaut `http://localhost:5000`) et que l'API tourne. 2. **Quel code d'erreur est renvoyé ?** → `LicenseResponse.ErrorCode` + `ErrorMessage`. Corréler avec le tableau C.3. 3. **Le client existe-t-il en base ?** → `LicenceRepository.GetClientByCodeAsync` → table `ClientBase` (`CL_Code`). 4. **La licence existe-t-elle pour ce client ?** → `LicenceRepository.GetByNumeroAndClientAsync` → table `Licence` (`LI_Numero` + `LI_IDClient`). 5. **La licence est-elle active / non expirée / non déjà attribuée ?** → colonnes `LI_Active`, `LI_DateExpiration`, `LI_IDMobile`. 6. **Le token est-il bien émis ?** → `LicenseEndpoints.BuildSession` + `TokenService.GenerateToken` (clé `MobileAuth:SigningKey` obligatoire). ## 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 tables `ClientBase`/`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`) ou `GetByPhysicalIdentityAsync` retourne `null`. - 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.json` est chargé **uniquement en Development/Test** (via `HfSqlTestConfigurationLoader.AddLocalTestConfiguration`). Il contient les connexions `Hfsql` (Business), `LicenseConnection`, `SyncConnection`, `ServiceConnection`, la clé `MobileAuth:SigningKey` et les flags `E2E`. - ⚠️ **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`) ```powershell # 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 : ```powershell .\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é.* ```