fix: checklist step 5, secondary-profile migrations and Linux database paths (#390) #394

Merged
maximus merged 5 commits from issue-390-manual-flag-profile-migrations into main 2026-10-07 02:00:03 +00:00
11 changed files with 164 additions and 17 deletions
Showing only changes of commit 02fca2ad3c - Show all commits

View file

@ -1,7 +1,16 @@
---
paths: ["**/migrations/**", "**/*.sql", "**/lib.rs"]
paths: ["**/migrations/**", "**/*.sql", "**/lib.rs", "**/profile_db.rs"]
---
Ne JAMAIS modifier une migration SQL existante. Toujours creer une nouvelle migration.
Les checksums sont verifies au demarrage (SHA-384 dans `_sqlx_migrations`).
Les migrations sont inline dans `lib.rs` via `tauri_plugin_sql::Migration`.
Les checksums sont verifies au demarrage (SHA-384 dans `_sqlx_migrations`) et epingles par le test `migration_checksums_match_the_released_ones`.
Les migrations sont inline dans `lib.rs`, rendues par `migrations()` via `tauri_plugin_sql::Migration`.
Le schema consolide (`consolidated_schema.sql`) sert uniquement pour les nouveaux profils.
Depuis #390 (ADR 0021), chaque nouvelle migration atteint deux chemins :
- le profil par defaut, migre par tauri-plugin-sql (sqlx) ;
- les profils secondaires, rattrapes a l'ouverture par `database::profile_db` (rusqlite), cles etrangeres actives comme sous sqlx.
Une migration doit donc rester correcte sur les deux, et :
- tout changement de schema se reporte dans `consolidated_schema.sql` (le test des etats publies compare chaque profil rattrape a un profil neuf) ;
- toute correction de donnees (seed, mots-cles, categories) se reporte AUSSI dans `consolidated_schema.sql` : un nouveau profil nait avec l'historique complet et ne rejoue jamais les migrations ;
- ne pas ajouter de marqueur a `infer_floor` : il ne sert qu'aux bases creees avant #390.

View file

@ -2,6 +2,12 @@
## [Non publié]
### Corrigé
- Classer une transaction à la main sur la page Transactions coche désormais l'étape « Classer une transaction à la main » de la liste de démarrage. L'application enregistrait cette marque sous une forme que la liste ne savait pas compter. Les transactions classées à la main avant cette version sont comptées dès l'ouverture du profil, et une sauvegarde exportée entre-temps les restaure correctement (#390).
- Les profils autres que le profil par défaut reçoivent désormais les mises à jour de leur base de données. Jusqu'ici, chacun gardait la structure de base de la version qui l'avait créé, et des fonctions ajoutées depuis pouvaient y échouer : dans un profil créé avant la 0.15.0, par exemple, le format d'import enregistré sur une source. À l'ouverture d'un tel profil, l'application garde d'abord une copie de sa base dans le dossier `migration-backups`, à côté des bases, puis applique les mises à jour manquantes. La copie est supprimée avec le profil, et chaque mise à jour est inscrite dans les journaux (Paramètres → Système) (#390).
- Sous Linux, supprimer un profil supprime désormais sa base de données, qui restait sur le disque dans `~/.config/com.simpl.resultat/` ; le fichier d'un profil supprimé avec la version 0.17.0 ou antérieure peut être supprimé à la main. Les colonnes de rendement des comptes du Bilan affichent désormais les rendements sous Linux, où chaque cellule affichait un tiret (#390).
## [0.17.0] - 2026-10-05
### Ajouté

View file

@ -2,6 +2,12 @@
## [Unreleased]
### Fixed
- Categorizing a transaction by hand on the Transactions page now ticks the "Categorize a transaction by hand" step of the getting-started checklist. The app stored that mark in a form the checklist could not count. Transactions you categorized by hand before this version are counted as soon as the profile is opened, and a backup exported in the meantime restores them correctly (#390).
- Profiles other than the default one now receive database updates. Until now each one kept the database layout of the version that created it, so features added since could fail in it: in a profile created before 0.15.0, for instance, the import format saved on a source. When such a profile is opened, the app first keeps a copy of its database in the `migration-backups` folder next to the databases, then applies the missing updates. The copy is deleted with the profile, and each update is written to the logs (Settings → System) (#390).
- On Linux, deleting a profile now deletes its database, which used to stay on disk in `~/.config/com.simpl.resultat/`; the file of a profile deleted with version 0.17.0 or earlier can be deleted by hand. The return columns of the Balance sheet accounts now show the returns on Linux, where every cell showed a dash (#390).
## [0.17.0] - 2026-10-05
### Added

View file

@ -65,11 +65,12 @@ src-tauri/
│ ├── commands/ # 3 modules, 17 commandes Tauri
│ │ ├── fs_commands.rs # Système de fichiers (6 commandes)
│ │ ├── export_import_commands.rs # Export/import chiffré (5 commandes)
│ │ └── profile_commands.rs # Gestion des profils (6 commandes)
│ │ └── profile_commands.rs # Gestion des profils (8 commandes)
│ ├── database/ # Schémas SQL et migrations
│ │ ├── schema.sql # Schéma initial (v1)
│ │ ├── seed_categories.sql # Seed catégories (v2)
│ │ └── consolidated_schema.sql # Schéma complet (nouveaux profils)
│ │ ├── consolidated_schema.sql # Schéma complet (nouveaux profils)
│ │ └── profile_db.rs # Rattrapage des profils secondaires (ADR 0021)
│ ├── lib.rs # Point d'entrée, 17 migrations inline, plugins
│ └── main.rs
└── Cargo.toml
@ -120,7 +121,7 @@ src-tauri/
- **20 tables** SQLite, **24 index** (voir `docs/architecture.md` pour le détail). Le module Bilan en représente 7 tables (`balance_categories`, `balance_accounts`, `balance_snapshots`, `balance_snapshot_lines`, `balance_account_transfers`, puis `balance_securities` + `balance_snapshot_holdings` ajoutées en Étape 2 — détail par titre) et 9 index. Ces deux comptes sont inchangés depuis v13 : v14-v16 ajoutent 2 tables + 2 index (déjà comptés), v17 est un `ALTER TABLE` pur
- **17 migrations inline** dans `lib.rs` (v1→v17, via `tauri_plugin_sql::Migration`). Étape 2 du Bilan (détail par titre) : v14 (`balance_securities` + `balance_snapshot_holdings` + 2 index), v15 (`balance_accounts.kind` + `detailed_since` + backfill), v16 (conversion des comptes cotés existants en détaillés 1-position) — voir [ADR 0015](docs/adr/0015-balance-detail-par-titre.md). Import : v17 (`import_sources.amount_mode` + `sign_convention`, les deux avec `CHECK`, + `header_signature` + `template_id`) — voir [ADR 0019](docs/adr/0019-format-import-persiste.md)
- **Schéma consolidé** (`consolidated_schema.sql`) pour l'initialisation des nouveaux profils
- **Schéma consolidé** (`consolidated_schema.sql`) pour l'initialisation des nouveaux profils, suivi de l'historique complet des migrations. tauri-plugin-sql ne migre que le profil par défaut : les profils secondaires rattrapent leurs migrations à chaque ouverture (`prepare_profile_db`, copie préalable, clés étrangères actives) — voir [ADR 0021](docs/adr/0021-migrations-profils-secondaires.md). Les bases vivent sous `app_config_dir()` (`profile_db_path`), qui n'est pas `app_data_dir()` sous Linux
- Les migrations appliquées sont protégées par checksum — ne jamais modifier une migration existante, toujours en créer une nouvelle
---

View file

@ -40,3 +40,7 @@ Les 6 migrations actuelles :
- **Immutabilité** : une migration appliquée ne peut jamais être modifiée (erreur de checksum), ce qui force la création de nouvelles migrations correctives
- **Synchronisation manuelle** : le fichier `consolidated_schema.sql` doit être mis à jour manuellement après chaque nouvelle migration
- **Pas de rollback** : les migrations sont unidirectionnelles (pas de `down`)
## Amendement #390 (2026-10-06)
Le schéma consolidé **crée** un profil secondaire, il ne le tient pas à jour. tauri-plugin-sql n'applique les migrations qu'à la base enregistrée par `add_migrations`, celle du profil par défaut (`simpl_resultat.db`). Jusqu'à #390, un profil secondaire gardait le schéma de la version qui l'avait créé. Depuis #390, un nouveau profil reçoit à sa création l'historique complet de `_sqlx_migrations`, et la commande `prepare_profile_db` applique à chaque ouverture les versions qui manquent à un profil secondaire. Voir l'[ADR 0021](0021-migrations-profils-secondaires.md). Les six migrations listées plus haut sont celles de 2025 ; il y en a dix-sept à la v0.17.0.

View file

@ -38,3 +38,12 @@ Architecture :
- **Pas de vue agrégée** : impossible de voir les données de tous les profils en même temps
- **Synchronisation du schéma** : chaque base doit être au même niveau de migration (géré par le plugin sql au démarrage)
- **Fichier `profiles.json` externe** : les métadonnées des profils ne sont pas dans SQLite, ce qui crée une dépendance à un fichier JSON séparé
## Amendement #390 (2026-10-06)
Deux affirmations des conséquences étaient fausses jusqu'à #390 :
- « Les migrations s'appliquent indépendamment à chaque base » et « géré par le plugin sql au démarrage » : le plugin ne migre que la base du profil par défaut. Un profil secondaire gardait le schéma de la version qui l'avait créé. Il rattrape désormais ses migrations à chaque ouverture ([ADR 0021](0021-migrations-profils-secondaires.md)).
- « Supprimer un profil = supprimer un fichier `.db` » : sous Linux, la commande cherchait la base sous `app_data_dir()` alors que le plugin la range sous `app_config_dir()`, et le fichier restait sur le disque. La suppression vise désormais le dossier du plugin et emporte aussi les fichiers annexes de SQLite et les copies prises avant un rattrapage.
`profiles.json` reste dans le répertoire de données de l'application (`app_data_dir()`) ; les bases sont dans le répertoire de configuration (`app_config_dir()`), qui n'est le même dossier que sous Windows et macOS.

View file

@ -0,0 +1,90 @@
# ADR 0021 — Les profils secondaires rattrapent leurs migrations à l'ouverture
- Status: **Accepted**
- Date: 2026-10-06
- Issues: #390
- Modifie l'[ADR 0003](0003-sqlx-migrations.md) (le schéma consolidé crée un profil, il ne le tient pas à jour) et l'[ADR 0005](0005-multi-profile-db.md) (« les migrations s'appliquent indépendamment à chaque base » était faux pour les profils secondaires)
## Contexte
Le constat de départ est un bug d'onboarding : classer une transaction à la main ne cochait pas la case 5 de la liste de démarrage. `updateTransactionCategory` liait le booléen JS `true`, que tauri-plugin-sql 2.3.2 transmet à sqlx comme une valeur JSON, donc stockée en texte `'true'`. La liste compte `is_manually_categorized = 1` et ne trouvait rien. Corriger les valeurs déjà en base par une migration v18 a fait apparaître deux défauts plus anciens.
**Les profils secondaires ne reçoivent aucune migration.** Le plugin associe la liste des migrations à une URL (`add_migrations("sqlite:simpl_resultat.db", …)`) et la retire au premier `Database.load`. Un profil secondaire (`profile_<id>.db`, depuis la v0.3.0) est créé par `initializeNewProfileDb` avec le schéma consolidé de la version qui le crée, puis n'en reçoit plus aucune. Un profil secondaire créé avant la v0.15.0 n'a donc pas les colonnes de format de `import_sources` (v17), et un profil créé avant la v0.10.0 n'a ni `balance_securities` ni `balance_accounts.kind`. Les commentaires de `consolidated_schema.sql` et de `get_new_profile_init_sql`, ainsi que l'ADR 0005, affirmaient le contraire.
**Trois commandes cherchaient les bases au mauvais endroit sous Linux.** Le plugin range les bases sous `app_config_dir()` (`wrapper.rs` : `DbPool::connect` puis `path_mapper`). `repair_migrations`, `delete_profile_db` et `compute_account_return` les cherchaient sous `app_data_dir()`, là où vivent `profiles.json` et les fichiers de licence. C'est le même dossier sous Windows et macOS, pas sous Linux (`~/.config/com.simpl.resultat` et `~/.local/share/com.simpl.resultat`). Sous Linux, `repair_migrations` ne faisait donc rien, supprimer un profil laissait sa base sur le disque, et le rendement d'un compte du Bilan échouait.
## Décision
**À chaque ouverture d'un profil, avant `Database.load`, la commande `prepare_profile_db` amène un profil secondaire à la dernière migration, avec la même liste que le profil par défaut, et répare les drapeaux texte de tous les profils.** Neuf règles la tiennent ; le code est dans `src-tauri/src/database/profile_db.rs`.
### 1. Une seule liste de migrations
`migrations()` (lib.rs) sert le plugin et le rattrapage. Le test `migration_checksums_match_the_released_ones` épingle le SHA-384 de chaque version, calculé avant que la liste soit sortie de `run()`. L'historique d'un profil secondaire vit dans la même table `_sqlx_migrations` que celui du profil par défaut, au format sqlx : même DDL, même checksum.
### 2. Un nouveau profil naît avec son historique
`get_new_profile_init_sql` renvoie le schéma consolidé, puis `history_sql`, qui inscrit toutes les versions avec leur checksum. Le plugin exécute les deux dans le même flux. Un profil créé depuis #390 n'a donc jamais rien à déduire : les versions suivantes l'atteignent par le rattrapage.
### 3. Un profil sans historique : un plancher déduit du schéma
Les profils créés avant #390 n'ont pas d'historique. Ils forment un ensemble fermé, un état par schéma consolidé qu'une version publiée a livré, et chaque état est conservé en fixture sous `src-tauri/src/database/fixtures/` :
| État | Versions qui l'ont créé | Plancher |
|---|---|---|
| A1 | v0.3.0 à v0.8.3, sauf v0.4.0 | 6 |
| A2 | v0.4.0 (seed à trois niveaux, retiré en v0.4.1) | 6 |
| B | v0.8.4 | 8 |
| C et D | v0.9.0, v0.9.1 (même schéma ; v11 ne touche que des données) | 10 |
| E | v0.10.0 à v0.14.0 | 16 |
| F | v0.15.0 à v0.17.x | 17 |
`infer_floor` reconnaît l'état par la dernière colonne ou table ajoutée, en partant de la plus récente. Le plancher vient du schéma, jamais de l'historique : une première écriture interrompue ne peut donc pas faire passer une version inférieure pour une version à appliquer. C'est ce qui interdit de rejouer le seed v2 (`DELETE FROM categories`) sur une base remplie. Un schéma qu'aucun marqueur ne reconnaît n'est pas touché. Aucun marqueur ne s'ajoute après la v17, puisqu'aucune base sans historique ne peut porter une version postérieure.
### 4. Une copie avant la première écriture
Quand une version reste à appliquer, la base est d'abord copiée par `VACUUM INTO` vers `<dossier des bases>/migration-backups/<profil>-before-v<N>.db`. Le chemin est lié, jamais formaté dans le SQL, ce qui tient un dossier comme `C:\Users\L'Heureux`. La copie est écrite en `.tmp`, synchronisée sur disque, puis renommée : un fichier final existant est donc complet, et il est gardé tel quel. Une copie qui échoue arrête tout avant la moindre écriture. La copie est supprimée avec le profil.
Elle reste dans le dossier des bases, pas dans `~/Documents/Simpl-Resultat/backups/` que la sauvegarde manuelle utilise : ce dossier est souvent synchronisé vers un service en ligne (OneDrive sous Windows), et une copie automatique en clair d'une base financière n'a rien à y faire.
### 5. Des écritures atomiques
L'historique, soit la table et chaque version jusqu'au plancher, est écrit en une transaction. Chaque version à appliquer s'exécute ensuite dans sa propre transaction. La première erreur arrête le rattrapage et les versions déjà appliquées restent inscrites.
### 6. Les clés étrangères restent actives
Comme sqlx pour le profil par défaut. Une version qui échoue sur une clé étrangère est reprise une fois sans elles. Les lignes qu'elle laisse sans parent repassent à NULL quand leur clé déclare `ON DELETE SET NULL` ; toute autre violation annule la version. Le cas réel est la v7, qui ajoute trois sous-catégories sous la catégorie 31 : un profil A dont les catégories viennent d'une restauration plus récente n'a plus de catégorie 31, et les trois sous-catégories deviennent des catégories racines. Couper les clés pour tout le rattrapage aurait laissé ces parents introuvables, et la restauration d'une sauvegarde de ce profil aurait échoué.
### 7. L'échec reste ouvert
Un rattrapage interrompu est écrit au Journal et le profil s'ouvre comme avant #390. Les messages nomment des fichiers, jamais le dossier : il contient le nom d'utilisateur du système, et une ligne d'erreur du Journal peut être jointe à une rétroaction (Loi 25).
### 8. Un seul dossier des bases
`profile_db_path` résout `app_config_dir()`, le dossier du plugin, pour les quatre commandes qui ouvrent une base. Il n'accepte que les deux formes de nom que l'application a jamais données : `simpl_resultat.db` et `profile_<hex>.db`.
### 9. Les drapeaux texte sont réparés partout
À chaque ouverture, dans tous les profils, `'true'` devient 1 et tout autre texte 0. L'opération est idempotente. Côté TypeScript, `toSqlFlag` empêche d'écrire de nouveau un booléen, y compris à la restauration d'une sauvegarde qui porte encore `"true"`.
## Conséquences
### Positives
- Un profil secondaire ancien retrouve le schéma courant à sa prochaine ouverture, et chaque migration future l'atteint.
- Sous Linux, supprimer un profil supprime enfin sa base, ses fichiers annexes et ses copies.
- Les règles sont éprouvées sur les sept états réels, avec des données d'utilisateur : comptes du Bilan convertis par v12 à v16, source d'import complétée par v17, drapeau réparé.
### Négatives et points de vigilance
- **Une correction de données livrée seulement en migration n'atteint pas un nouveau profil**, qui naît avec l'historique complet. Elle doit aussi entrer dans `consolidated_schema.sql` (règle `.claude/rules/sql-migrations.md`). Le test qui compare chaque état rattrapé à un profil neuf ne regarde que le schéma.
- **Deux moteurs appliquent les mêmes migrations** : sqlx pour le profil par défaut, rusqlite pour les autres. Le SQL est le même et les clés étrangères sont actives des deux côtés. La reprise sans clés étrangères n'existe que du côté rusqlite.
- **La commande est synchrone.** Tauri l'exécute sur le fil principal, ce qui sérialise deux ouvertures du même profil (React StrictMode monte deux fois en développement). L'interface attend pendant un rattrapage, une fois par profil ancien, et la durée est écrite au Journal.
- **Le test qui distingue les deux dossiers ne voit l'écart que sous Linux**, la plateforme de la CI. La vérification à la main sous Linux reste due.
- **Les profils supprimés sous Linux avant ce correctif** ont laissé leur base dans `~/.config/com.simpl.resultat/`. Rien ne supprime automatiquement un fichier que `profiles.json` ne cite pas : un `profiles.json` abîmé coûterait alors de vrais profils.
## Alternatives écartées
- **Une migration v18 de données**, proposée par le corps de #390 : elle n'aurait réparé que le profil par défaut.
- **Enregistrer chaque profil auprès du plugin au démarrage** (`add_migrations` par URL dans `setup`) : l'état des migrations du plugin est privé et figé au démarrage, un profil créé en cours de session n'y figurerait pas, et sqlx aurait rejoué v1 et v2 sur une base sans historique.
- **Un rattrapage côté TypeScript** : le plugin n'expose aucune transaction (voir `db.ts`), et ni `VACUUM INTO` ni les clés étrangères ne seraient testables hors de Tauri.
- **Des clés étrangères coupées pendant tout le rattrapage** : les actions `ON DELETE` ne joueraient plus comme sur le profil par défaut, et les lignes sans parent feraient échouer la restauration d'une sauvegarde de ce profil.

View file

@ -44,7 +44,7 @@ simpl-resultat/
│ ├── pages/ # 14 pages (dont 4 sous-pages rapports)
│ ├── services/ # 25 services métier (19 détaillés dans la section « Services TypeScript »)
│ ├── shared/ # Types, constantes, matrice d'entitlements (entitlements.ts), gate profils (profileGate.ts), CTA d'upsell (upsellCta.ts), URL d'achat (purchaseUrl.ts), échéance de licence (licenseExpiry.ts)
│ ├── utils/ # 13 utilitaires purs (parsing montants/dates, détection CSV, codec de format d'import, dictionnaire lexical, signatures de banques, charts)
│ ├── utils/ # 14 utilitaires purs (parsing montants/dates, détection CSV, codec de format d'import, dictionnaire lexical, signatures de banques, charts, drapeaux SQL 0/1)
│ ├── i18n/ # Config i18next + locales FR/EN
│ ├── __fixtures__/ # csv/ : corpus de détection (15 cas figés) ; samples/ : gabarits du dossier d'exemple embarqués par Rust
│ ├── App.tsx # Router principal
@ -61,7 +61,9 @@ simpl-resultat/
│ │ ├── database/ # Schémas SQL et migrations
│ │ │ ├── schema.sql
│ │ │ ├── seed_categories.sql
│ │ │ └── consolidated_schema.sql
│ │ │ ├── consolidated_schema.sql
│ │ │ ├── profile_db.rs # Rattrapage des profils secondaires (#390, ADR 0021)
│ │ │ └── fixtures/ # Schémas de création des profils publiés (tests)
│ │ ├── lib.rs # Point d'entrée, migrations, plugins
│ │ └── main.rs
│ ├── capabilities/ # Permissions Tauri
@ -164,13 +166,22 @@ Les migrations sont définies inline dans `src-tauri/src/lib.rs` via `tauri_plug
v17 est purement additive : elle n'ajoute **ni table ni index** (20 tables / 24 index inchangés). Le `CHECK` d'`amount_mode` admet `absolute_indicator` dès maintenant, pour que le troisième mode de montant puisse être livré sans nouvelle migration, alors que l'application le **refuse** encore explicitement (voir plus bas). `LIKE` plutôt que `json_extract` : la migration ne dépend d'aucune extension JSON1 dans le SQLite embarqué. `sign_convention` n'est délibérément pas backfillée — son `DEFAULT` restitue la valeur que le code codait en dur, la seule convention passée inférable.
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).
La liste est rendue par `migrations()` (lib.rs) ; tauri-plugin-sql ne l'applique qu'à la base du **profil par défaut** (`simpl_resultat.db`, seule URL passée à `add_migrations`). Un test épingle le SHA-384 de chaque version.
Les **profils secondaires** (`profile_<id>.db`) sont créés par `get_new_profile_init_sql` : le fichier `consolidated_schema.sql`, qui porte le schéma complet, puis `history_sql`, qui inscrit toutes les versions dans `_sqlx_migrations` au format sqlx. Le plugin ne les migre jamais. Depuis #390, `prepare_profile_db` les rattrape à chaque ouverture, avant `Database.load` ([ADR 0021](adr/0021-migrations-profils-secondaires.md), `src-tauri/src/database/profile_db.rs`) :
- une base sans historique (créée avant #390) reçoit un **plancher** déduit de son schéma (`infer_floor`) : 6, 8, 10, 16 ou 17 selon la version qui l'a créée, tous rejoués en test depuis `database/fixtures/`. Aucune version au plus égale au plancher n'est rejouée ;
- avant la première version à appliquer, une copie `VACUUM INTO` est prise dans `migration-backups/` à côté des bases, puis supprimée avec le profil ;
- chaque version s'applique dans sa transaction, clés étrangères actives comme sous sqlx. Une version qui bute sur une clé est reprise une fois sans elles, et ses lignes sans parent repassent à NULL si la clé déclare `ON DELETE SET NULL` ;
- la même commande répare dans tous les profils les drapeaux `is_manually_categorized` stockés en texte (`'true'` → 1).
Le dossier des bases est celui du plugin : `app_config_dir()`, résolu par `profile_db_path` (`profile_commands.rs`). Il diffère de `app_data_dir()`, où vit `profiles.json`, sous Linux seulement (`~/.config/com.simpl.resultat` et `~/.local/share/com.simpl.resultat`).
## Services TypeScript (19)
| Service | Responsabilité |
|---------|---------------|
| `db.ts` | Wrapper de connexion (tauri-plugin-sql) |
| `db.ts` | Wrapper de connexion (tauri-plugin-sql). `connectToProfile` appelle `repair_migrations` puis `prepare_profile_db` avant `Database.load`, et écrit au Journal ce que la préparation a changé (`describePreparation`, noms de fichiers seulement, #390) ; un échec est écrit au Journal et le profil s'ouvre quand même |
| `profileService.ts` | Gestion des profils |
| `categoryService.ts` | CRUD catégories hiérarchiques |
| `transactionService.ts` | CRUD et filtrage des transactions ; détection d'erreurs FK RESTRICT pour transactions liées à un compte de bilan (typed `TransactionLinkedToBalanceError`) ; `getTransactionCount` (compte brut de la table, sans filtre — signal des états vides de Transactions et Budget, #351/#353) |
@ -340,7 +351,7 @@ Depuis le chantier #297-#301, l'accès aux modules est gaté par l'édition de l
Modules Free — jamais gatés, aucune `FeatureKey` : Dashboard, Import, Transactions, Catégories, hub `/reports` + `/reports/trends`, Export/Import, Paramètres, Changelog, docs. Le déclassement d'édition est **non destructif** : les données des modules verrouillés restent en base et redeviennent accessibles avec une clé valide.
## Commandes Tauri (38)
## Commandes Tauri (39)
### `fs_commands.rs` — Système de fichiers (7)
@ -360,15 +371,18 @@ Modules Free — jamais gatés, aucune `FeatureKey` : Dashboard, Import, Transac
- `read_import_file` — Lecture fichier chiffré
- `is_file_encrypted` — Vérification magic SREF
### `profile_commands.rs` — Gestion des profils (7)
### `profile_commands.rs` — Gestion des profils (8)
Les commandes qui ouvrent une base passent par `profile_db_path` : dossier du plugin (`app_config_dir()`) et nom de fichier validé (`simpl_resultat.db` ou `profile_<hex>.db`, #390).
- `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é
- `delete_profile_db` — Suppression de la base d'un profil secondaire, de ses fichiers annexes SQLite (`-wal`, `-shm`, `-journal`) et de ses copies `migration-backups/` (#390)
- `get_new_profile_init_sql` — Schéma consolidé suivi de l'historique complet des migrations (`history_sql`, #390)
- `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)
- `prepare_profile_db` — Avant chaque ouverture : rattrapage des migrations d'un profil secondaire (copie préalable) et réparation des drapeaux de classement manuel de tout profil. Rend un rapport sans chemin (`ProfilePreparation`). Synchrone, donc sérialisée sur le fil principal — [ADR 0021](adr/0021-migrations-profils-secondaires.md)
### `license_commands.rs` — Licence et activation machine (11)
@ -427,7 +441,7 @@ Module privé appelé uniquement par `auth_commands.rs` et `license_commands.rs`
### `balance_commands.rs` — Bilan (1)
- `compute_account_return(account_id, period_start, period_end, db_filename)` — Calcul Modified Dietz d'un compte sur une période. Ouvre une connexion `rusqlite` courte sur le fichier DB du profil actif, lit le snapshot ≤ `period_start`, le snapshot ≥ `period_end` et tous les `balance_account_transfers` JOIN `transactions` dans la fenêtre, puis appelle `return_calculator::modified_dietz`. Retourne `AccountReturn { value_start, value_end, net_contributions, return_pct, annualized_pct, is_partial, has_no_transfers_warning }`. Voir [ADR 0008](adr/0008-modified-dietz-pour-rendement.md).
- `compute_account_return(account_id, period_start, period_end, db_filename)` — Calcul Modified Dietz d'un compte sur une période. Ouvre une connexion `rusqlite` courte sur le fichier DB du profil actif (résolu par `profile_db_path` ; sous Linux, l'ancien chemin `app_data_dir()` faisait échouer le calcul, #390), lit le snapshot ≤ `period_start`, le snapshot ≥ `period_end` et tous les `balance_account_transfers` JOIN `transactions` dans la fenêtre, puis appelle `return_calculator::modified_dietz`. Retourne `AccountReturn { value_start, value_end, net_contributions, return_pct, annualized_pct, is_partial, has_no_transfers_warning }`. Voir [ADR 0008](adr/0008-modified-dietz-pour-rendement.md).
Le module privé `return_calculator.rs` (déclaré dans `commands/mod.rs` mais non exposé comme commande) contient la logique pure Modified Dietz et ses tests `#[cfg(test)] mod tests` co-localisés (TDD, 7 cas : nominal / pas de snapshot début / partial / créé en cours / vidé / sans transferts / annualisation).
@ -480,6 +494,7 @@ Les modules payants sont enveloppés dans des **layout-routes pathless `RequireF
- **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
- **Préparation du profil** : `prepare_profile_db` (Rust/rusqlite) rattrape les migrations d'un profil secondaire et répare les drapeaux texte avant le chargement ; un échec est écrit au Journal, le profil s'ouvre quand même (#390)
- **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 |
@ -590,3 +605,4 @@ Les ADRs documentent les décisions techniques structurantes. Ils vivent dans `d
| [0018](adr/0018-suppression-advisories-non-atteignables.md) | Suppression d'advisories non atteignables : admission sur preuve par cible livrée, clé par ID, garde-fou bloquant | 2026-07-27 | Accepted |
| [0019](adr/0019-format-import-persiste.md) | Le format d'import est une donnée persistée intégralement, jamais re-devinée | 2026-08-13 | Accepted |
| [0020](adr/0020-identifiant-machine-derive.md) | Identifiant machine dérivé : un hachage à clé fixe remplace l'identifiant du système auprès du serveur de licences | 2026-10-01 | Accepted |
| [0021](adr/0021-migrations-profils-secondaires.md) | Les profils secondaires rattrapent leurs migrations à l'ouverture | 2026-10-06 | Accepted |

View file

@ -61,6 +61,8 @@ Gérez plusieurs profils indépendants, chacun avec sa propre base de données.
- Un profil par défaut est créé automatiquement au premier lancement
- Le NIP est demandé à chaque fois que vous accédez à un profil protégé
- La suppression d'un profil supprime définitivement toutes ses données — cette action est irréversible
- À l'ouverture d'un profil créé par une version antérieure, l'application met sa base à jour après en avoir gardé une copie dans le dossier `migration-backups`, à côté des bases (sous Linux `~/.config/com.simpl.resultat/`, sous Windows `%APPDATA%\com.simpl.resultat\`). La copie est supprimée avec le profil
- Sous Linux, les versions jusqu'à la 0.17.0 laissaient sur le disque la base d'un profil supprimé (`~/.config/com.simpl.resultat/profile_….db`) ; vous pouvez supprimer ce fichier à la main
---

View file

@ -874,7 +874,9 @@
"tips": [
"A default profile is automatically created on first launch",
"The PIN is requested each time you access a protected profile",
"Deleting a profile permanently removes all its data — this action cannot be undone"
"Deleting a profile permanently removes all its data — this action cannot be undone",
"When you open a profile created by an earlier version, the app updates its database after keeping a copy in the migration-backups folder, next to the databases (on Linux ~/.config/com.simpl.resultat/, on Windows %APPDATA%\\com.simpl.resultat\\). The copy is deleted with the profile",
"On Linux, versions up to 0.17.0 left a deleted profile's database on disk (~/.config/com.simpl.resultat/profile_….db); you can delete that file by hand"
]
},
"dashboard": {

View file

@ -874,7 +874,9 @@
"tips": [
"Un profil par défaut est créé automatiquement au premier lancement",
"Le NIP est demandé à chaque fois que vous accédez à un profil protégé",
"La suppression d'un profil supprime définitivement toutes ses données — cette action est irréversible"
"La suppression d'un profil supprime définitivement toutes ses données — cette action est irréversible",
"À l'ouverture d'un profil créé par une version antérieure, l'application met sa base à jour après en avoir gardé une copie dans le dossier migration-backups, à côté des bases (sous Linux ~/.config/com.simpl.resultat/, sous Windows %APPDATA%\\com.simpl.resultat\\). La copie est supprimée avec le profil",
"Sous Linux, les versions jusqu'à la 0.17.0 laissaient sur le disque la base d'un profil supprimé (~/.config/com.simpl.resultat/profile_….db) ; vous pouvez supprimer ce fichier à la main"
]
},
"dashboard": {