Files
Omika/docs/ARCHITECTURE_PROJET.md
T
2026-10-23 18:46:50 +02:00

1115 lines
57 KiB
Markdown

# 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<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.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<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()` :
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 <token>`,
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é.*
```