New /reports/cartes page surfaces a dashboard-style snapshot of the reference month: - 4 KPI cards (income / expenses / net / savings rate) showing MoM and YoY deltas simultaneously, each with a 13-month sparkline highlighting the reference month - 12-month income vs expenses overlay chart (bars + net balance line) - Top 5 category increases + top 5 decreases MoM, clickable through to the category zoom report - Budget adherence card: on-target count + 3 worst overruns with progress bars - Seasonality card: reference month vs same calendar month averaged over the two previous years, with deviation indicator All data is fetched in a single getCartesSnapshot() service call that runs four queries concurrently (25-month flow, MoM category deltas, budget-vs-actual, seasonality). Missing months are filled with zeroes in the sparklines but preserved as null in the MoM/YoY deltas so the UI can distinguish "no data" from "zero spend". - Exported pure helpers: shiftMonth, defaultCartesReferencePeriod - 13 vitest cases covering zero data, MoM/YoY computation, January wrap-around, missing-month handling, division by zero for the savings rate, seasonality with and without history, top mover sign splitting and 5-cap Note: src/components/reports/CompareReferenceMonthPicker.tsx is a temporary duplicate — the canonical copy lives on the issue-96 branch (refactor: compare report). Once both branches merge the content is identical and git will dedupe. Keeping the local copy here means the Cartes branch builds cleanly on main without depending on #96. Closes #97 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
19 KiB
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/TXTread_file_content— Lecture avec gestion de l'encodagehash_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 lignespick_folder— Dialogue de sélection de dossier
export_import_commands.rs — Export/Import de données (5)
pick_save_file— Dialogue de sauvegardepick_import_file— Dialogue de sélection de fichierwrite_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 depuisprofiles.jsonsave_profiles— Sauvegarde de la configurationdelete_profile_db— Suppression du fichier de base de donnéesget_new_profile_init_sql— Récupération du schéma consolidéhash_pin— Hachage Argon2id du PIN (formatargon2id: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 datastore_activation_token— Stockage du token d'activationread_license— Lecture de la licence stockéeget_edition— Détection de l'édition active (free/base/premium)get_machine_id— Génération d'un identifiant machine uniqueactivate_machine— Activation en ligne (appel API serveur de licences, issue #49)deactivate_machine— Désactivation d'une machine enregistréelist_activated_machines— Liste des machines activées pour la licenceget_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 Logtorefresh_auth_token— Rafraîchit l'access token via le refresh tokenget_account_info— Lecture du cache d'affichage (viaaccount_cache::load_unverified, accepte les payloads legacy)check_subscription_status— Vérifie l'abonnement (max 1×/jour, fallback cache gracieux). Déclenche aussi la migrationtokens.json→ keychain viatoken_store::loadlogout— 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"ounull. Utilisé par la bannière de sécuritéTokenStoreFallbackBannerdans 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}dansaccount.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é parlicense_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_TIERSdansentitlements.rs. Modifier cette constante pour changer les gates, jamais ailleurs dans le code - Temporaire :
auto-updateest ouvert àfreeen attendant le serveur de licences (issue #49). À re-gater à[base, premium]quand l'activation payante sera live
- Source de vérité :
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+) :
- Frontend appelle
start_oauth→ génère un code verifier PKCE (64 chars), le stocke dansOAuthState(Mutex en mémoire du processus), retourne l'URL Logto - Frontend ouvre l'URL via
tauri-plugin-opener→ le navigateur système affiche la page Logto - L'utilisateur s'authentifie (ou Logto auto-consent si session existante) → redirection 303 vers
simpl-resultat://auth/callback?code=... - L'OS route le custom scheme vers une nouvelle instance de l'app →
tauri-plugin-single-instance(featuredeep-link) détecte l'instance existante, ne démarre PAS un nouveau processus, et forwarde l'URL à l'instance vivante - Le callback
app.deep_link().on_open_url(...)enregistré viaDeepLinkExtreçoit les URLs. Pour chaque URL :- Si un param
codeest présent → appellehandle_auth_callback(token exchange vers/oidc/token, fetch/oidc/me, écriture des tokens viatoken_store::save(keychain OS, fallback fichier 0600) + cache signé viaaccount_cache::save(HMAC-SHA256), émission de l'eventauth-callback-success) - Si un param
errorest présent → émission de l'eventauth-callback-erroravecerror: error_description
- Si un param
- Le hook
useAuth(frontend) écouteauth-callback-success/auth-callback-erroret met à jour l'état
Pourquoi cet enchaînement est critique :
- Sans
tauri-plugin-single-instance: une nouvelle instance démarre à chaque callback, leOAuthStateest vide (pas de verifier), le token exchange échoue - Sans
on_open_url: l'ancien listenerapp.listen("deep-link://new-url", ...)ne recevait pas les URLs forwardées par single-instance. L'API canonique v2 viaDeepLinkExtest 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<App />dansmain.tsx, attrape les crashs React et afficheErrorPageen fallbackErrorPage: 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.tsxapplique un timeout de 10 secondes surconnectActiveProfile()— afficheErrorPageau 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_migrationsavant le chargement de la DB - Log viewer :
logService.tscapture lesconsole.log/warn/errordans un buffer circulaire (500 entrées, persisté ensessionStorage), affiché dans la page Paramètres viaLogViewerCard
| 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 :
- build-windows (windows-latest) → Installeur
.exe(NSIS) - 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