1115 lines
57 KiB
Markdown
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é.*
|
|
```
|