# Architecture technique — Simpl'Résultat > Document mis à jour le 2026-04-13 — Version 0.7.3 ## Stack technique | Couche | Technologie | Version | |--------|------------|---------| | Framework desktop | Tauri | v2 | | Frontend | React | 19.1 | | Langage frontend | TypeScript | 5.8 | | Bundler | Vite | 6.4 | | CSS | Tailwind CSS | v4 | | Backend | Rust (via Tauri) | stable | | Base de données | SQLite (tauri-plugin-sql) | — | | Graphiques | Recharts | 3.7 | | Icônes | Lucide React | 0.563 | | i18n | i18next + react-i18next | 25.8 / 16.5 | | Drag & Drop | @dnd-kit | 6.3 / 10.0 | | CSV | PapaParse | 5.5 | | Chiffrement | aes-gcm (Rust) | 0.10 | | Hachage PIN | Argon2 (Rust) | 0.5 | ## Structure du projet ``` simpl-resultat/ ├── src/ # Frontend React/TypeScript │ ├── components/ # 58 composants organisés par domaine │ │ ├── adjustments/ # 3 composants │ │ ├── budget/ # 5 composants │ │ ├── categories/ # 5 composants │ │ ├── dashboard/ # 2 composants │ │ ├── import/ # 13 composants (wizard d'import) │ │ ├── layout/ # AppShell, Sidebar │ │ ├── profile/ # 3 composants (PIN, formulaire, switcher) │ │ ├── reports/ # ~25 composants (hub, faits saillants, tendances, comparables, zoom catégorie) │ │ ├── settings/ # 5 composants (+ LogViewerCard, LicenseCard, AccountCard) │ │ ├── shared/ # 6 composants réutilisables │ │ └── transactions/ # 5 composants │ ├── contexts/ # ProfileContext (état global profil) │ ├── hooks/ # 18+ hooks custom (useReducer, 5 hooks rapports par domaine) │ ├── pages/ # 14 pages (dont 4 sous-pages rapports) │ ├── services/ # 14 services métier │ ├── shared/ # Types et constantes partagés │ ├── utils/ # 4 utilitaires (parsing, CSV, charts) │ ├── i18n/ # Config i18next + locales FR/EN │ ├── App.tsx # Router principal │ └── main.tsx # Point d'entrée ├── src-tauri/ # Backend Rust │ ├── src/ │ │ ├── commands/ # 6 modules de commandes Tauri │ │ │ ├── fs_commands.rs │ │ │ ├── export_import_commands.rs │ │ │ ├── profile_commands.rs │ │ │ ├── license_commands.rs │ │ │ ├── auth_commands.rs │ │ │ └── entitlements.rs │ │ ├── database/ # Schémas SQL et migrations │ │ │ ├── schema.sql │ │ │ ├── seed_categories.sql │ │ │ └── consolidated_schema.sql │ │ ├── lib.rs # Point d'entrée, migrations, plugins │ │ └── main.rs │ ├── capabilities/ # Permissions Tauri │ └── Cargo.toml ├── .github/workflows/ # CI/CD │ └── release.yml ├── docs/ # Documentation technique └── config/ # Configuration ``` ## Base de données ### Tables (13) | Table | Description | |-------|-------------| | `import_sources` | Configuration des sources d'import CSV | | `imported_files` | Suivi des fichiers importés (hash anti-doublons) | | `categories` | Catégories hiérarchiques (dépenses/revenus) | | `suppliers` | Fournisseurs avec auto-catégorisation | | `keywords` | Mots-clés pour catégorisation automatique | | `transactions` | Transactions individuelles | | `adjustments` | Ajustements manuels (ponctuels ou récurrents) | | `adjustment_entries` | Montants par catégorie pour chaque ajustement | | `budget_entries` | Allocations budgétaires mensuelles par catégorie | | `budget_templates` | Modèles de budget réutilisables | | `budget_template_entries` | Catégories et montants dans les modèles | | `import_config_templates` | Modèles prédéfinis de config d'import | | `user_preferences` | Préférences applicatives (clé-valeur) | ### Index (9) Index sur : `transactions` (date, category, supplier, source, file, parent), `categories` (parent, type), `suppliers` (category, normalized_name), `keywords` (category, keyword), `budget_entries` (year, month), `adjustment_entries` (adjustment_id), `imported_files` (source). ## Système de migrations Les migrations sont définies inline dans `src-tauri/src/lib.rs` via `tauri_plugin_sql::Migration` : | # | Version | Description | |---|---------|-------------| | 1 | v1 | Schéma initial (13 tables) | | 2 | v2 | Seed des catégories et mots-clés | | 3 | v3 | Ajout `has_header` sur `import_sources` | | 4 | v4 | Ajout `is_inputable` sur `categories` | | 5 | v5 | Création de `import_config_templates` | | 6 | v6 | Changement contrainte unique `imported_files` (hash → filename) | | 7 | v7 | Ajout sous-catégories d'assurance (niveau 3) | Pour les **nouveaux profils**, le fichier `consolidated_schema.sql` contient le schéma complet avec toutes les migrations pré-appliquées (pas besoin de rejouer les migrations). ## Services TypeScript (17) | Service | Responsabilité | |---------|---------------| | `db.ts` | Wrapper de connexion (tauri-plugin-sql) | | `profileService.ts` | Gestion des profils | | `categoryService.ts` | CRUD catégories hiérarchiques | | `transactionService.ts` | CRUD et filtrage des transactions | | `importSourceService.ts` | Configuration des sources d'import | | `importedFileService.ts` | Suivi des fichiers importés | | `importConfigTemplateService.ts` | Modèles de configuration d'import | | `categorizationService.ts` | Catégorisation automatique + helpers édition de mot-clé (`validateKeyword`, `previewKeywordMatches`, `applyKeywordWithReassignment`) | | `adjustmentService.ts` | Gestion des ajustements | | `budgetService.ts` | Gestion budgétaire | | `dashboardService.ts` | Agrégation données tableau de bord | | `reportService.ts` | Génération de rapports : `getMonthlyTrends`, `getCategoryOverTime`, `getHighlights`, `getCompareMonthOverMonth`, `getCompareYearOverYear`, `getCategoryZoom` (CTE récursive bornée anti-cycle), `getCartesSnapshot` (snapshot dashboard Cartes, requêtes parallèles) | | `dataExportService.ts` | Export de données (chiffré) | | `userPreferenceService.ts` | Stockage préférences utilisateur | | `logService.ts` | Capture des logs console (buffer circulaire, sessionStorage) | | `licenseService.ts` | Validation et gestion de la clé de licence (appels commandes Tauri) | | `authService.ts` | OAuth2 PKCE / Compte Maximus (appels commandes Tauri auth_*) | ## Hooks (14) Chaque hook encapsule la logique d'état via `useReducer` : | Hook | Domaine | |------|---------| | `useCategories` | Catégories avec hiérarchie | | `useTransactions` | Transactions et filtrage | | `useDataImport` | Import de données | | `useImportWizard` | Assistant d'import multi-étapes | | `useImportHistory` | Historique des imports | | `useAdjustments` | Ajustements | | `useBudget` | Budget | | `useDashboard` | Métriques du tableau de bord | | `useReportsPeriod` | Période de reporting synchronisée via query string (bookmarkable) | | `useHighlights` | Panneau de faits saillants du hub rapports | | `useTrends` | Rapport Tendances (sous-vue flux global / par catégorie) | | `useCompare` | Rapport Comparables (mode MoM / YoY / budget) | | `useCategoryZoom` | Rapport Zoom catégorie avec rollup sous-catégories | | `useCartes` | Rapport Cartes (snapshot KPI + sparklines + top movers + budget + saisonnalité via `getCartesSnapshot`) | | `useDataExport` | Export de données | | `useTheme` | Thème clair/sombre | | `useUpdater` | Mise à jour de l'application (gated par entitlement licence) | | `useLicense` | État de la licence et entitlements | | `useAuth` | Authentification Compte Maximus (OAuth2 PKCE, subscription status) | ## Commandes Tauri (35) ### `fs_commands.rs` — Système de fichiers (6) - `scan_import_folder` — Scan récursif de dossier pour fichiers CSV/TXT - `read_file_content` — Lecture avec gestion de l'encodage - `hash_file` — Hash SHA-256 (détection de doublons) - `detect_encoding` — Détection auto (UTF-8, Windows-1252, ISO-8859-15) - `get_file_preview` — Aperçu des N premières lignes - `pick_folder` — Dialogue de sélection de dossier ### `export_import_commands.rs` — Export/Import de données (5) - `pick_save_file` — Dialogue de sauvegarde - `pick_import_file` — Dialogue de sélection de fichier - `write_export_file` — Écriture fichier chiffré (format SREF) - `read_import_file` — Lecture fichier chiffré - `is_file_encrypted` — Vérification magic SREF ### `profile_commands.rs` — Gestion des profils (7) - `load_profiles` — Chargement depuis `profiles.json` - `save_profiles` — Sauvegarde de la configuration - `delete_profile_db` — Suppression du fichier de base de données - `get_new_profile_init_sql` — Récupération du schéma consolidé - `hash_pin` — Hachage Argon2id du PIN (format `argon2id:salt:hash`) - `verify_pin` — Vérification du PIN (supporte Argon2id et legacy SHA-256 pour rétrocompatibilité) - `repair_migrations` — Réparation des checksums de migration (rusqlite) ### `license_commands.rs` — Licence et activation machine (10) - `validate_license_key` — Validation offline d'une clé de licence (JWT Ed25519) - `store_license` — Stockage de la clé dans le répertoire app data - `store_activation_token` — Stockage du token d'activation - `read_license` — Lecture de la licence stockée - `get_edition` — Détection de l'édition active (free/base/premium) - `get_machine_id` — Génération d'un identifiant machine unique - `activate_machine` — Activation en ligne (appel API serveur de licences, issue #49) - `deactivate_machine` — Désactivation d'une machine enregistrée - `list_activated_machines` — Liste des machines activées pour la licence - `get_activation_status` — État d'activation de la machine courante ### `auth_commands.rs` — Compte Maximus / OAuth2 PKCE (5) - `start_oauth` — Génère un code verifier PKCE et retourne l'URL d'authentification Logto - `refresh_auth_token` — Rafraîchit l'access token via le refresh token - `get_account_info` — Lecture du cache d'affichage (via `account_cache::load_unverified`, accepte les payloads legacy) - `check_subscription_status` — Vérifie l'abonnement (max 1×/jour, fallback cache gracieux). Déclenche aussi la migration `tokens.json` → keychain via `token_store::load` - `logout` — Efface tokens (`token_store`) + cache signé (`account_cache`) + clé HMAC du keychain Note : `handle_auth_callback` n'est PAS exposée comme commande — elle est appelée depuis le handler deep-link `on_open_url` dans `lib.rs`. Voir section "OAuth2 et deep-link" plus bas. ### `token_store.rs` — Stockage des tokens OAuth (1) - `get_token_store_mode` — Retourne `"keychain"`, `"file"` ou `null`. Utilisé par la bannière de sécurité `TokenStoreFallbackBanner` dans Settings pour alerter l'utilisateur quand les tokens sont dans le fallback fichier. Module non-command : `save`, `load`, `delete`, `store_mode` — toute la logique de persistance passe par ce module, `auth_commands.rs` ne touche jamais directement `tokens.json`. Voir l'ADR 0006 pour la conception complète. ### `account_cache.rs` — Cache d'abonnement signé (aucune commande) Module privé appelé uniquement par `auth_commands.rs` et `license_commands.rs`. Expose : - `save(app, &AccountInfo)` — écrit l'enveloppe signée `{data, sig}` dans `account.json`, avec clé HMAC-SHA256 stockée dans le keychain. - `load_unverified(app)` — lecture pour affichage UI (accepte legacy et signé). - `load_verified(app)` — lecture pour gating licence (refuse legacy, tampering, absence de clé). Utilisé par `license_commands::check_account_edition`. - `delete(app)` — efface le fichier et la clé HMAC du keychain. ### `entitlements.rs` — Entitlements (1) - `check_entitlement` — Vérifie si une feature est autorisée selon l'édition - Source de vérité : `FEATURE_TIERS` dans `entitlements.rs`. Modifier cette constante pour changer les gates, jamais ailleurs dans le code - Temporaire : `auto-update` est ouvert à `free` en attendant le serveur de licences (issue #49). À re-gater à `[base, premium]` quand l'activation payante sera live ## Plugins Tauri Ordre d'initialisation dans `lib.rs` (certains plugins ont des contraintes d'ordre) : | Plugin | Rôle | Contrainte | |--------|------|-----------| | `tauri-plugin-single-instance` | Empêche les doubles lancements et forwarde les URLs deep-link au processus existant | **Doit être le premier plugin** ; feature `deep-link` requise pour le forwarding d'URL | | `tauri-plugin-opener` | Ouverture d'URLs externes et de fichiers | — | | `tauri-plugin-dialog` | Dialogues de sélection de fichier/dossier | — | | `tauri-plugin-process` | Relaunch après mise à jour | — | | `tauri-plugin-deep-link` | Gère le scheme custom `simpl-resultat://` | Doit être initialisé avant `setup()` pour que `on_open_url` soit disponible | | `tauri-plugin-updater` | Mise à jour auto (gated par entitlement `auto-update`) | Initialisé dans `setup()` derrière `#[cfg(desktop)]` | | `tauri-plugin-sql` | SQLite + migrations | Doit être initialisé avec les migrations pour que le schéma soit prêt | ## OAuth2 et deep-link (Compte Maximus) Flow complet (v0.7.3+) : 1. Frontend appelle `start_oauth` → génère un code verifier PKCE (64 chars), le stocke dans `OAuthState` (Mutex en mémoire du processus), retourne l'URL Logto 2. Frontend ouvre l'URL via `tauri-plugin-opener` → le navigateur système affiche la page Logto 3. L'utilisateur s'authentifie (ou Logto auto-consent si session existante) → redirection 303 vers `simpl-resultat://auth/callback?code=...` 4. L'OS route le custom scheme vers une nouvelle instance de l'app → `tauri-plugin-single-instance` (feature `deep-link`) détecte l'instance existante, **ne démarre PAS un nouveau processus**, et forwarde l'URL à l'instance vivante 5. Le callback `app.deep_link().on_open_url(...)` enregistré via `DeepLinkExt` reçoit les URLs. Pour chaque URL : - Si un param `code` est présent → appelle `handle_auth_callback` (token exchange vers `/oidc/token`, fetch `/oidc/me`, écriture des tokens via `token_store::save` (keychain OS, fallback fichier 0600) + cache signé via `account_cache::save` (HMAC-SHA256), émission de l'event `auth-callback-success`) - Si un param `error` est présent → émission de l'event `auth-callback-error` avec `error: error_description` 6. Le hook `useAuth` (frontend) écoute `auth-callback-success` / `auth-callback-error` et met à jour l'état Pourquoi cet enchaînement est critique : - **Sans `tauri-plugin-single-instance`** : une nouvelle instance démarre à chaque callback, le `OAuthState` est vide (pas de verifier), le token exchange échoue - **Sans `on_open_url`** : l'ancien listener `app.listen("deep-link://new-url", ...)` ne recevait pas les URLs forwardées par single-instance. L'API canonique v2 via `DeepLinkExt` est nécessaire - **Sans gestion des erreurs** : un callback `?error=...` laissait l'UI bloquée en état "loading" infini Fichiers : `src-tauri/src/lib.rs` (wiring), `src-tauri/src/commands/auth_commands.rs` (PKCE + token exchange), `src-tauri/src/commands/token_store.rs` (persistance keychain + fallback), `src-tauri/src/commands/account_cache.rs` (cache signé HMAC), `src/hooks/useAuth.ts` (frontend), `src/components/settings/TokenStoreFallbackBanner.tsx` (UI de l'état dégradé). ## Pages et routing Le routing est défini dans `App.tsx`. Toutes les pages sont englobées par `AppShell` (sidebar + layout). L'accès est contrôlé par `ProfileContext` (gate). ### Gestion d'erreurs - **`ErrorBoundary`** (class component) : wrape `` dans `main.tsx`, attrape les crashs React et affiche `ErrorPage` en fallback - **`ErrorPage`** : page d'erreur réutilisable avec détails techniques (collapsible), bouton "Actualiser", vérification de mises à jour, et liens de contact/issues - **Timeout au démarrage** : `App.tsx` applique un timeout de 10 secondes sur `connectActiveProfile()` — affiche `ErrorPage` au lieu d'un spinner infini si la connexion DB échoue - **Retry au démarrage** : `connectActiveProfile()` réessaie jusqu'à 3 fois avec 1s de délai avant d'afficher l'erreur - **Réparation de migrations** : `repair_migrations` (Rust/rusqlite) supprime les checksums invalides de `_sqlx_migrations` avant le chargement de la DB - **Log viewer** : `logService.ts` capture les `console.log/warn/error` dans un buffer circulaire (500 entrées, persisté en `sessionStorage`), affiché dans la page Paramètres via `LogViewerCard` | Route | Page | Description | |-------|------|-------------| | `/` | `DashboardPage` | Tableau de bord (résumé, pie chart, budget vs réel, dépenses dans le temps) | | `/import` | `ImportPage` | Assistant d'import CSV | | `/transactions` | `TransactionsPage` | Liste avec filtres | | `/categories` | `CategoriesPage` | Gestion hiérarchique | | `/adjustments` | `AdjustmentsPage` | Ajustements manuels | | `/budget` | `BudgetPage` | Planification budgétaire | | `/reports` | `ReportsPage` | Hub des rapports : panneau faits saillants + 4 cartes de navigation | | `/reports/highlights` | `ReportsHighlightsPage` | Faits saillants détaillés (soldes, top mouvements, top transactions) | | `/reports/trends` | `ReportsTrendsPage` | Tendances (flux global + par catégorie) | | `/reports/compare` | `ReportsComparePage` | Comparables (MoM / YoY / Réel vs budget) | | `/reports/category` | `ReportsCategoryPage` | Zoom catégorie avec rollup + édition contextuelle de mots-clés | | `/reports/cartes` | `ReportsCartesPage` | Tableau de bord KPI avec sparklines, top movers, budget et saisonnalité | | `/settings` | `SettingsPage` | Paramètres | | `/docs` | `DocsPage` | Documentation in-app | | `/changelog` | `ChangelogPage` | Historique des versions (bilingue FR/EN) | Page spéciale : `ProfileSelectionPage` (affichée quand aucun profil n'est actif). ## Internationalisation - **Librairie** : i18next + react-i18next - **Langue par défaut** : Français (`fr`) - **Langue de fallback** : Anglais (`en`) - **Fichiers** : `src/i18n/locales/fr.json`, `src/i18n/locales/en.json` - **Clés organisées** hiérarchiquement par domaine (`nav.*`, `dashboard.*`, `import.*`, etc.) ## CI/CD Deux workflows Forgejo Actions (avec miroir GitHub) dans `.forgejo/workflows/` : ### `check.yml` — Vérifications sur branches et PR Déclenché sur chaque push de branche (sauf `main`) et chaque PR vers `main`. Lance en parallèle : - `cargo check` + `cargo test` (Rust) - `npm run build` (tsc + vite) - `npm test` (vitest) Doit être vert avant tout merge. Évite de découvrir des régressions au moment du tag de release. ### `release.yml` — Build et publication Déclenché par les tags `v*`. Deux jobs : 1. **build-windows** (windows-latest) → Installeur `.exe` (NSIS) 2. **build-linux** (ubuntu-22.04) → `.deb` + `.rpm` Fonctionnalités : - Signature des binaires (clés TAURI_SIGNING_PRIVATE_KEY) - JSON d'updater publié sur `https://git.lacompagniemaximus.com/api/packages/maximus/generic/simpl-resultat/latest/latest.json` - Release Forgejo automatique avec assets et release notes extraites du CHANGELOG.md