diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md
index 5ec5e0f..bd6389e 100644
--- a/CHANGELOG.fr.md
+++ b/CHANGELOG.fr.md
@@ -3,6 +3,7 @@
## [Non publié]
### Ajouté
+- **Bilan — documentation et ADRs** (`docs/`) : finalise le milestone Bilan avec la passe documentaire. `docs/architecture.md` répertorie désormais les 5 nouvelles tables (`balance_categories`, `balance_accounts`, `balance_snapshots`, `balance_snapshot_lines`, `balance_account_transfers`), les 7 nouveaux index, les invariants CHECK et FK (CAD seulement, invariants de type, `RESTRICT` sur `transaction_id` pour la reproductibilité Modified Dietz), le découpage 4 sections de `balance.service.ts` (CRUD / snapshots+lignes / rendements+transferts / prix), les 3 hooks scoped par page (`useBalanceAccounts`, `useSnapshotEditor`, `useBalanceOverview`), la commande Tauri `compute_account_return` (avec mention de la future commande `fetch_price` Phase 5), et les 3 nouvelles routes `/balance*`. Trois nouveaux ADRs accompagnent : **0008 — Modified Dietz** (justifie le choix vs ROI / TWR / IRR avec référence à `return_calculator.rs`) ; **0009 — Proxy price-fetching via maximus-api** (architecture documentée maintenant, implémentation BLOQUÉE en attendant la Phase 2 de maximus-api — couvre les considérations privacy comme le strip de headers, l'absence de corrélation `(symbole, licence)` dans les logs et le User-Agent fixe `simpl-resultat`, l'abstraction adapter Yahoo + CoinGecko, la stratégie d'auth Bearer, le rate-limiting client + serveur et le double gating premium UI + serveur) ; **0010 — FK RESTRICT sur `balance_account_transfers.transaction_id`** (justifie l'arbitrage intégrité vs friction pour la reproductibilité Modified Dietz). Le guide utilisateur gagne une nouvelle section *Bilan* qui détaille la saisie de snapshot (simple + coté), la liaison de transferts, la lecture des rendements multi-horizons (3M / 1A / depuis création avec colonne non-ajustée côte à côte), avec la mention « à venir Phase 5 » pour le price-fetching premium. Clés i18n `docs.balance.*` (FR + EN) ajoutées pour que le guide in-app reflète la nouvelle section (#145)
- **Bilan — suite de tests d'intégration cross-cutting** (infrastructure de tests) : clôt la feature *Bilan* avec une couche de tests d'intégration qui exerce toute la surface TypeScript en un seul flux de bout en bout (compte → catégorie cotée → snapshot coté → transfert lié → rendement) et des assertions dédiées sur le verrou de devise (CAD seulement au MVP, refusé à la fois côté service et côté CHECK SQL), la sécurité de tolérance pour le type coté (un mauvais enregistrement ne doit PAS supprimer les lignes existantes), le câblage de `computeAccountReturn` (résolution du profil actif, transmission des dates ISO, conservation telle quelle d'une réponse de période partielle). Trois nouveaux tests Rust d'intégration appliquent la migration v9 par-dessus un schéma v1 seedé contenant déjà des transactions pour vérifier (1) aucune perte ni mutation de données, (2) le round-trip lier / délier sur de vraies `transaction_id`, (3) la chaîne FK RESTRICT (suppression d'une transaction liée bloquée, autorisée après détachement), (4) la cohabitation indépendante des espaces d'identifiants `categories.id` (v1) et `balance_categories.id` (v9). Un test de non-régression au niveau source sur `TransactionTable.tsx` verrouille le contrat de l'icône de transfert inlinée : prop optionnelle, court-circuit en chaînage optionnel, clés i18n, aria-label, layout partagé de la cellule description — pour que la page reste rendue à l'identique en l'absence de transferts liés. (#144)
- **Bilan — rendements Modified Dietz et liaison de transferts** (route `/balance`) : le rendement par compte arrive enfin. Nouveau module Rust `commands/return_calculator.rs` qui implémente la formule Modified Dietz `R = (V_fin − V_début − ΣCF_i) / (V_début + ΣW_i × CF_i)` avec pondération des apports à la précision du jour `W_i = (T − t_i) / T`, et annualisation `(1 + R)^(365/T) − 1`. Les cas limites — snapshot d'extrémité manquant, aucun flux taggé sur la période, compte créé en cours de période, vidé puis rechargé, période de durée nulle — sont surfacés via les flags explicites `is_partial` / `has_no_transfers_warning` pour que l'UI affiche un tiret + tooltip clair plutôt qu'un nombre incompréhensible. Nouvelle commande Tauri `compute_account_return(account_id, period_start, period_end)` qui exécute trois lectures SQL courtes contre la BD du profil actif (dernier snapshot ≤ début de période, dernier snapshot ≤ fin de période, transferts joints aux transactions filtrés sur la période) puis alimente le calculateur. Sept tests Rust co-localisés en TDD couvrent chaque cas avant l'implémentation. Le tableau des comptes sur `/balance` affiche désormais quatre colonnes supplémentaires côte à côte : 3M / 1A / Depuis création (Modified Dietz) plus une colonne *Non ajusté* qui calcule simplement `(V_fin − V_début) / V_début` pour qu'on voie d'un coup d'œil quelle part du rendement vient de la pondération des apports. Le menu d'actions de chaque ligne reçoit l'item *Lier transferts* qui ouvre une modal de sélection multiple avec filtres période / catégorie / recherche texte ; la modal propose automatiquement le sens (`in` pour les montants bancaires négatifs, `out` pour les positifs) et l'utilisateur peut inverser ligne par ligne avant de soumettre. Les transactions liées à un ou plusieurs comptes de bilan affichent maintenant une petite icône `Link2` à côté de la description dans la page *Transactions*, avec un tooltip listant les noms et sens des comptes. Les chemins de suppression en lot (par fichier importé et tout effacer) pré-vérifient l'existence d'un lien dans `balance_account_transfers` et surfacent l'erreur typée `TransactionLinkedToBalanceError` (« Cette transaction est liée au compte de bilan X — déliez-la avant de supprimer ») au lieu de laisser fuiter l'erreur SQLite brute. Le graphique d'évolution sur `/balance` superpose désormais des lignes verticales de référence à chaque date de transfert lié (vert pour `in`, rouge pour `out`). Nouvelles clés i18n sous `balance.returns.*`, `balance.accountsTable.*`, `balance.transfers.*`, `balance.evolution.*`, `transactions.transferIcon.*` (FR + EN) (#142)
- **Bilan — page `/balance` avec graphique d'évolution et entrée sidebar** (route `/balance`) : quatrième tranche de la feature *Bilan*, qui la rend enfin accessible depuis la navigation. La nouvelle page compose (1) une carte d'aperçu avec la valeur nette agrégée du dernier snapshot, le Δ% par rapport au snapshot chronologiquement précédent (affiché « — » quand il n'existe qu'un seul snapshot), un avertissement de fraîcheur quand le dernier snapshot date de plus de 60 jours, et un CTA *Nouveau snapshot* qui pointe vers `/balance/snapshot` ; (2) un sélecteur de période (3 mois / 6 mois / 1 an / 3 ans / Tout) qui recharge toutes les séries en parallèle ; (3) un graphique d'évolution avec deux modes — *Ligne* (une seule série `SUM(value) GROUP BY snapshot_date`) et *Empilé par catégorie* (une `` Recharts par `balance_categories.key`) ; (4) un tableau des comptes listant chaque compte actif avec sa dernière valeur snapshot, le Δ% par compte sur la période active (valeur la plus récente vs valeur du premier snapshot dans la fenêtre — null si pas d'ancrage, affiché « — »), et un menu d'actions (Détail désactivé en attendant la #142, Archiver). Les colonnes de rendement (3M / 1A / depuis création / non ajusté) sont réservées pour une version ultérieure avec un commentaire `TODO`. La sidebar expose désormais l'entrée *Bilan* (icône `Wallet`) entre *Rapports* et *Paramètres*. Le service gagne trois helpers de série temporelle : `getSnapshotTotalsByDate(range?)`, `getSnapshotTotalsByCategoryAndDate(range?)`, `getAccountsLatestSnapshot()` ainsi qu'un calcul d'ancrage par compte `getAccountsPeriodAnchor(range)` — tous couverts par des tests unitaires. Nouveau hook `useBalanceOverview` (`useReducer` scoped) qui pilote l'état de la page. Nouvelles clés i18n sous `balance.overview.*`, `balance.period.*`, `balance.chart.*` plus `nav.balance` (FR + EN) (#141)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d48aa3f..fbfa459 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
## [Unreleased]
### Added
+- **Balance sheet — documentation and ADRs** (`docs/`): closes the Bilan milestone with the documentation pass. `docs/architecture.md` now lists the 5 new tables (`balance_categories`, `balance_accounts`, `balance_snapshots`, `balance_snapshot_lines`, `balance_account_transfers`), the 7 new indexes, the SQL CHECK and FK invariants (CAD-only, kind invariants, `RESTRICT` on `transaction_id` for Modified Dietz reproducibility), the `balance.service.ts` 4-section layout (CRUD / snapshots+lines / returns+transfers / prices), the 3 page-scoped hooks (`useBalanceAccounts`, `useSnapshotEditor`, `useBalanceOverview`), the `compute_account_return` Tauri command (with the `fetch_price` future-Phase-5 mention), and the 3 new `/balance*` routes. Three new ADRs land alongside: **0008 — Modified Dietz** (justifies the choice vs. ROI / TWR / IRR with reference to `return_calculator.rs`); **0009 — Proxy price-fetching via maximus-api** (architecture documented now, implementation stays BLOCKED by maximus-api Phase 2 — covers privacy considerations like header stripping, no `(symbol, license)` log correlation and the fixed `simpl-resultat` UA, the Yahoo + CoinGecko provider abstraction, the Bearer auth strategy, the client + server rate limiting and the dual-side premium gating); **0010 — FK RESTRICT on `balance_account_transfers.transaction_id`** (justifies the integrity over friction trade-off for Modified Dietz reproducibility). The user guide gains a new *Balance sheet* section walking through snapshot entry (simple + priced), transfer linking, multi-horizon return reading (3M / 1Y / since inception with the side-by-side unadjusted column), with the price-fetching premium flagged "coming in Phase 5". `docs.balance.*` i18n keys (FR + EN) ship so the in-app guide reflects the new section (#145)
- **Balance sheet — cross-cutting integration test suite** (test infrastructure): closes out the *Bilan* feature with a layer of integration tests that exercise the whole TypeScript surface in a single happy-path flow (account → priced category → priced snapshot → linked transfer → return) plus dedicated assertions for currency lock (CAD-only at the MVP, rejected at both the service layer and SQL CHECK), priced-kind tolerance safety (a bad save must NOT clear pre-existing lines), `computeAccountReturn` wiring (active-profile resolution, ISO date forwarding, partial-period payload pass-through). Three new Rust integration tests apply migration v9 on top of a seeded v1 schema with pre-existing transactions to verify (1) no row loss / data mutation, (2) link / unlink transfer round-trip on real transaction ids, (3) the FK RESTRICT chain (linked transaction deletion blocked, unblocked after unlink), (4) the v1 `categories.id` and v9 `balance_categories.id` namespaces coexist independently. A non-regression source-level test on `TransactionTable.tsx` locks down the inlined transfer icon contract: optional prop, optional-chaining short-circuit, i18n keys, aria-label, shared description-cell layout — so the page renders identically when no transfers are linked. (#144)
- **Balance sheet — Modified Dietz returns and transfer linking** (route `/balance`): per-account performance now ships. New Rust module `commands/return_calculator.rs` implements the Modified Dietz formula `R = (V_end − V_start − ΣCF_i) / (V_start + ΣW_i × CF_i)` with day-precision contribution weights `W_i = (T − t_i) / T`, plus `(1 + R)^(365/T) − 1` annualization. Edge cases — missing endpoint snapshot, no flows tagged in the period, account created mid-period, depleted-then-refilled, zero-length period — are surfaced with explicit `is_partial` / `has_no_transfers_warning` flags so the UI shows a clean dash + tooltip instead of a confusing number. The new Tauri command `compute_account_return(account_id, period_start, period_end)` runs three short SQL reads against the active profile DB (latest snapshot ≤ period start, latest snapshot ≤ period end, transfers JOINed with transactions filtered to the period) and feeds the calculator. Seven co-located TDD tests cover every case before the implementation. The accounts table on `/balance` now shows four extra columns side-by-side: 3M / 1Y / Since-inception (Modified Dietz) plus an *Unadjusted* column showing the simple `(V_end − V_start) / V_start` so the user can see at a glance how much of the return came from contribution timing. Each row's actions menu gains a *Link transfers* item that opens a multi-select modal with date range / category / free-text filters; the modal auto-proposes the direction (`in` for negative bank amounts, `out` for positive) and the user can flip it per row before submitting. Transactions linked to one or more balance accounts now show a small `Link2` icon next to the description in the *Transactions* page, with a tooltip listing the account name(s) and direction(s). Bulk transaction-deletion paths (per-imported-file and clear-all) now pre-check for any link in `balance_account_transfers` and surface a typed `TransactionLinkedToBalanceError` ("This transaction is linked to balance account X — unlink it before deleting") instead of leaking the raw SQLite FK error. The evolution chart on `/balance` now overlays vertical reference lines at every linked-transfer date (green for `in`, red for `out`). New i18n keys under `balance.returns.*`, `balance.accountsTable.*`, `balance.transfers.*`, `balance.evolution.*`, `transactions.transferIcon.*` (FR + EN) (#142)
- **Balance sheet — `/balance` overview page, evolution chart and sidebar entry** (route `/balance`): fourth slice of the *Bilan* feature finally surfaces it in the navigation. The new page composes (1) an overview card with the latest aggregate net worth, the Δ% versus the previous chronological snapshot (rendered as "—" when only one snapshot exists), a 60-day staleness warning when the latest snapshot is older than that threshold, and a *New snapshot* CTA pointing at `/balance/snapshot`; (2) a period selector (3 months / 6 months / 1 year / 3 years / All) that re-fetches every series in parallel; (3) an evolution chart with two modes — *Line* (single series of `SUM(value) GROUP BY snapshot_date`) and *Stacked by category* (one Recharts `` per `balance_categories.key`); (4) an accounts table listing every active account with its latest snapshot value, the per-account Δ% over the active period (latest value vs the value at the earliest snapshot inside the window — null when no anchor exists, rendered as "—"), and an actions menu (Details placeholder, Archive). Return-metric columns (3M / 1Y / since-creation / unadjusted) are reserved for a later release with a `TODO` marker. The sidebar now exposes the *Balance sheet* entry (`Wallet` icon) between *Reports* and *Settings*. The service grows three time-series helpers: `getSnapshotTotalsByDate(range?)`, `getSnapshotTotalsByCategoryAndDate(range?)`, `getAccountsLatestSnapshot()` and a per-account anchor query `getAccountsPeriodAnchor(range)` — all guarded by unit tests. New `useBalanceOverview` hook (scoped `useReducer`) drives the page state. New i18n keys under `balance.overview.*`, `balance.period.*`, `balance.chart.*` plus `nav.balance` (FR + EN) (#141)
diff --git a/docs/adr/0008-modified-dietz-pour-rendement.md b/docs/adr/0008-modified-dietz-pour-rendement.md
new file mode 100644
index 0000000..a0a58aa
--- /dev/null
+++ b/docs/adr/0008-modified-dietz-pour-rendement.md
@@ -0,0 +1,100 @@
+# ADR 0008 — Modified Dietz pour le calcul du rendement par compte
+
+- Status: Accepted
+- Date: 2026-04-25
+- Milestone: `overnight-2026-04-26-bilan` (Issues #138 → #145)
+
+## Context
+
+La feature Bilan introduit une vue patrimoniale (snapshots datés) avec calcul du rendement par compte. Le rendement réel d'un compte d'investissement n'est PAS `(V_fin − V_début) / V_début` : cette formule confond les **gains réels** avec les **apports/retraits**.
+
+> Exemple : compte CELI à 10 000 $, on dépose 5 000 $, le compte vaut 16 000 $ à la fin. La formule naïve donne 60 % (16/10), mais la moitié du gain est juste l'apport. Le vrai rendement est 6 % : `(16 000 − 5 000 − 10 000) / 10 000`.
+
+C'est exactement la raison pour laquelle l'utilisateur tague des transferts (table `balance_account_transfers`) : pour les exclure du calcul.
+
+Quatre formules candidates ont été comparées dans le spike (`~/claude-code/.spikes/bilan/code/rendement.md`) :
+
+| Méthode | Pondère le timing des flux ? | Nécessite des valeurs intermédiaires ? | Standard d'industrie ? |
+|---------|---|---|---|
+| ROI ajusté simple | ❌ | ❌ | ❌ |
+| **Modified Dietz** | ✅ (approximation linéaire) | ❌ | ✅ (GIPS-compliant en première approximation) |
+| Time-Weighted Return (TWR) | ✅ (exact) | ✅ (à chaque flux) | ✅ |
+| Money-Weighted Return / IRR | ✅ (exact, itératif) | ❌ | ✅ |
+
+Contraintes du contexte Simpl'Résultat :
+- Les snapshots sont saisis librement (mensuels, trimestriels, ad-hoc) — il n'y a **pas** de valeur du compte aux dates de flux.
+- Pas de solveur numérique embarqué côté client (pas de Newton-Raphson en Rust pour l'IRR).
+- L'utilisateur doit pouvoir comprendre le résultat sans formation financière.
+
+## Decision
+
+**Adopter Modified Dietz** comme méthode unique de calcul du rendement par compte au MVP, implémentée côté Rust dans le module privé `src-tauri/src/commands/return_calculator.rs`.
+
+```
+R = (V_fin − V_début − C_net) / (V_début + Σ(C_i × W_i))
+```
+
+où :
+- `C_i` = chaque flux (signé : + apport, − retrait)
+- `W_i = (T − t_i) / T` = poids temporel (1 si début de période, 0 si fin)
+- `T` = durée totale de la période en jours, `t_i` = position du flux
+
+### Architecture
+
+- **Logique pure** : `commands/return_calculator.rs` (module privé, pas exposé comme commande). `pub(crate) fn modified_dietz(...) -> AccountReturn`.
+- **Commande Tauri** : `commands/balance_commands.rs::compute_account_return(account_id, period_start, period_end, db_filename)` ouvre une connexion `rusqlite` courte sur la DB du profil actif, lit le snapshot ≤ start, le snapshot ≥ end et les cash flows liés, puis délègue le calcul.
+- **Dépendance Cargo** : `chrono = "0.4"` ajoutée pour l'arithmétique de dates (poids temporels en jours).
+- **Tests TDD co-localisés** : `#[cfg(test)] mod tests` dans le même fichier — 7 cas (nominal, pas de snapshot début, partial-end, compte créé en cours, compte vidé, aucun transfert, annualisation).
+
+### Output
+
+```rust
+struct AccountReturn {
+ value_start: Option,
+ value_end: f64,
+ net_contributions: f64,
+ return_pct: Option, // None si dénominateur ≈ 0
+ annualized_pct: Option, // (1 + R)^(365/days) - 1, si days > 30
+ is_partial: bool, // true si snapshot manquant après fin
+ has_no_transfers_warning: bool, // true si aucun transfert lié
+}
+```
+
+### Affichage côté UI (`BalanceAccountsTable`)
+
+- 3 colonnes Modified Dietz : 3M / 1A / depuis création
+- 1 colonne **rendement non-ajusté** (`(V_fin − V_début) / V_début`) côte-à-côte — pédagogique : montre l'effet des apports vs gains réels
+- Warnings visibles (`is_partial`, `has_no_transfers_warning`) avec tooltip i18n
+
+## Consequences
+
+### Positive
+
+- **Pas besoin de valeurs intermédiaires** : le calcul ne nécessite que les snapshots existants + les transferts taggés. C'est exactement ce que l'utilisateur saisit déjà.
+- **Standard d'industrie** : Modified Dietz est GIPS-compliant en première approximation. Le résultat est défendable.
+- **Pédagogique** : afficher le rendement non-ajusté à côté du Modified Dietz éduque l'utilisateur sur la différence entre "valeur du compte" et "vraie performance".
+- **Implémentation simple** : ~50 lignes de logique pure en Rust + 7 tests. Pas de solveur numérique.
+- **Reproductibilité** : combinée avec la FK `ON DELETE RESTRICT` sur `balance_account_transfers.transaction_id` (voir [ADR 0010](0010-fk-restrict-balance-transfers.md)), une période déjà calculée ne peut pas changer rétroactivement.
+
+### Negative / trade-offs
+
+- **Approximation** : Modified Dietz suppose une distribution linéaire des flux dans le temps. Si plusieurs flux concentrés tombent juste avant un mouvement de marché significatif, l'erreur s'accumule. Acceptable pour un usage personnel ; un investisseur professionnel utiliserait TWR exact.
+- **Cas dégénéré "compte vidé puis rechargé"** : le dénominateur `V_début + Σ(C_i × W_i)` peut tendre vers zéro et faire exploser le ratio. Mitigé par un warning UI "Performance non significative" basé sur `has_no_transfers_warning` ou un seuil sur le dénominateur.
+- **Pas de TWR au MVP** : si l'utilisateur veut la vraie performance gestionnaire (indépendante du timing des flux), il devra attendre une v2 qui demandera de saisir des valeurs intermédiaires aux dates de flux.
+- **Pas de Money-Weighted Return / IRR** : formule plus précise mais nécessite Newton-Raphson. Coût/bénéfice défavorable au MVP.
+
+## Alternatives considered
+
+- **ROI ajusté simple** (`(V_fin − V_début − C_net) / V_début`). Rejeté : ignore *quand* l'apport est arrivé. Un dépôt de 10 000 $ le 1er janvier vs le 31 décembre donne le même résultat — incorrect.
+- **TWR (Time-Weighted Return)**. Rejeté pour le MVP : nécessite des valeurs du compte aux dates de flux, qu'on ne stocke pas. Possible v2 si l'utilisateur accepte de saisir des valeurs intermédiaires.
+- **IRR (Money-Weighted Return)**. Rejeté : nécessite un solveur Newton-Raphson, complexité disproportionnée pour un usage personnel.
+- **Calcul côté TypeScript (sans commande Rust)**. Rejeté : l'arithmétique de dates en JavaScript (`Date.UTC(...) / 86400000`) est correcte mais le pattern projet (logique financière côté Rust avec tests `cargo`) est plus robuste. Cohérent avec `aes-gcm`, `argon2`, etc.
+
+## References
+
+- Spec : [`spec-decisions-bilan.md`](../../spec-decisions-bilan.md), [`spec-plan-bilan.md`](../../spec-plan-bilan.md)
+- Spike : `~/claude-code/.spikes/bilan/code/rendement.md` (comparaison ROI / Modified Dietz / TWR / IRR)
+- Implémentation : `src-tauri/src/commands/return_calculator.rs`, `src-tauri/src/commands/balance_commands.rs`
+- Tests TDD : `#[cfg(test)] mod tests` dans `return_calculator.rs` (7 cas)
+- ADR liée : [0010 — FK RESTRICT sur `balance_account_transfers.transaction_id`](0010-fk-restrict-balance-transfers.md)
+- GIPS standards (Global Investment Performance Standards) — Modified Dietz est listé comme méthode acceptable d'approximation pour des périodes < 1 an.
diff --git a/docs/adr/0009-proxy-price-fetching-via-maximus-api.md b/docs/adr/0009-proxy-price-fetching-via-maximus-api.md
new file mode 100644
index 0000000..73f92b1
--- /dev/null
+++ b/docs/adr/0009-proxy-price-fetching-via-maximus-api.md
@@ -0,0 +1,158 @@
+# ADR 0009 — Price-fetching premium via proxy maximus-api
+
+- Status: Accepted (architecture documentée — implémentation reportée à l'Issue #143, BLOCKED par maximus-api Phase 2)
+- Date: 2026-04-25
+- Milestone: `overnight-2026-04-26-bilan` (architecture spec)
+
+## Context
+
+La feature Bilan supporte des comptes "priced" (actions, crypto) où chaque ligne de snapshot stocke `(quantity, unit_price, value)`. La saisie manuelle de `unit_price` reste toujours possible mais devient pénible dès qu'on a plusieurs titres ou qu'on rétro-saisit un historique.
+
+L'objectif est de proposer un bouton "récupérer le prix au [date]" qui interroge un fournisseur de données (Yahoo Finance, CoinGecko, etc.) sans **trahir le principe privacy-first NON NÉGOCIABLE** du projet :
+
+> Zéro donnée envoyée vers un serveur tiers. Tout le traitement CSV et toutes les données financières restent en local. Aucune télémétrie, aucun analytics cloud.
+
+Or interroger Yahoo ou CoinGecko, c'est par définition envoyer une requête sortante depuis l'IP de l'utilisateur. Quelles informations fuiteraient ?
+
+- **L'IP de l'utilisateur** : géolocalisation grossière, profilage de session
+- **L'User-Agent par défaut** de `reqwest` : `reqwest/0.12 ...`, identifie le client comme une app Tauri (silhouette technique reconnaissable)
+- **Le symbole + date** : "AAPL au 2026-03-15" n'est pas identifiant en soi mais corrélé à l'IP, le provider peut reconstruire le portefeuille
+- **Headers résiduels** : `Accept-Language` peut révéler la locale système
+
+Trois architectures candidates :
+
+| Option | Privacy | Complexité serveur | Coût d'API |
+|--------|---------|--------------------|------------|
+| Appel direct client → provider | ❌ IP exposée, fingerprint headers | aucune | par user (rate limits triggered fast) |
+| Appel direct + Tor / VPN intégré | ⚠ partiel, latence dégradée | aucune | par user |
+| **Proxy via maximus-api auto-hébergé** | ✅ IP cachée, headers strippés, cache mutualisé | Endpoint `/v1/prices` à maintenir | mutualisé (cache mutualisé entre users premium) |
+
+## Decision
+
+**Implémenter le price-fetching comme fonctionnalité premium-only servie par `maximus-api` agissant comme proxy**, avec consentement explicite et hygiène de headers stricte des deux côtés du fil.
+
+### Architecture
+
+```
+[App Tauri]
+ │ GET /v1/prices?symbol=AAPL&date=2026-03-15
+ │ Headers: Authorization: Bearer
+ │ Accept: application/json
+ │ User-Agent: simpl-resultat
+ ▼
+[maximus-api] ← VPS Max (Coolify)
+ │ 1. Strip TOUS headers entrants identifiants
+ │ 2. Validation tier premium (403 si non-premium)
+ │ 3. Cache SQLite (symbol, date) → price (TTL infini sur dates passées)
+ │ 4. Cache miss → adapter (Yahoo / CoinGecko)
+ ▼
+[Provider tiers] ← voit l'IP du VPS, pas du client
+```
+
+### Choix de providers : abstraction adapter
+
+Côté maximus-api, un module `price-fetcher` expose une interface unique et délègue à des adapters :
+
+| Provider | Stocks | Crypto | Coût | Adapter |
+|----------|--------|--------|------|---------|
+| **Yahoo Finance** (unofficial) | ✅ | ⚠ | gratuit | `YahooAdapter` (HTTP direct) |
+| **CoinGecko** | ❌ | ✅ excellent | gratuit (free tier 30 req/min) | `CoinGeckoAdapter` |
+| Alpha Vantage (fallback) | ✅ | ⚠ | freemium | optionnel si Yahoo casse |
+
+**Stocks → Yahoo** ; **Crypto → CoinGecko**. L'abstraction permet de swap si un provider casse, sans changer le contrat client.
+
+### Stratégie d'authentification
+
+- **`Authorization: Bearer ` uniquement.** Le token est lu côté client depuis `activation_path` (le fichier déjà utilisé par `license_commands.rs` pour persister le token d'activation). **Jamais stocké dans `user_preferences`** (la table SQL de l'app n'a pas vocation à versionner les credentials).
+- **Jamais en query string.** Un token-in-URL leakerait dans :
+ - Les logs Traefik / nginx du VPS (URL complète loguée par défaut)
+ - Le header `Referer` si maximus-api redirige
+ - Les écrans de partage (le header `Authorization` est masqué par les outils de capture, pas l'URL)
+
+### Hygiène des headers — privacy en profondeur
+
+**Côté client (Rust / `reqwest`)** :
+- `reqwest::Client::builder().user_agent("simpl-resultat").build()` — UA fixe, pas le default `reqwest/0.12 ...`
+- Headers envoyés UNIQUEMENT : `Authorization: Bearer ` + `Accept: application/json`
+- **Pas** de `Accept-Language` (révèle la locale)
+- **Pas** d'autres headers identifiants
+
+**Côté serveur (maximus-api)** :
+- Strip TOUS les headers entrants avant de proxyer vers le provider tiers (`X-Forwarded-For`, `User-Agent` client, `Accept-Language`, etc.)
+- **Ne JAMAIS logger `(symbol, license_id)` ensemble.** Soit séparer les logs (un journal pour la facturation/quota par licence sans symbole, un journal pour les hits cache/provider sans license), soit hasher le `license_id` côté serveur avec un sel rotatif court avant log
+- Validation premium **AVANT** cache et provider — un client non-premium reçoit 403 sans qu'aucun appel sortant ne soit déclenché
+
+### Rate limiting
+
+**Côté client** :
+- Max 1 fetch / 2 secondes (timer simple)
+- Dedup in-flight par `(symbol, date)` (deux clics rapides = 1 seule requête réseau)
+- Backoff exponentiel sur 5xx / network : 2s, 4s, 8s — max 3 retries
+- Plafond hard : 100 fetches par session snapshot (anti-loop)
+
+**Côté serveur** :
+- Quota par licence (proposition initiale : 1000 req/jour, le cache absorbe l'essentiel)
+- Le cache `(symbol, date)` est immuable pour les dates passées (TTL infini), 5 min pour `today` (le marché peut bouger)
+
+### Premium gating — défense en profondeur
+
+- **UI client** : si `entitlements.check_entitlement("price-fetching")` retourne `false`, le bouton ↻ affiche un tooltip "Disponible avec abonnement" et est désactivé. Pas de tentative de fetch.
+- **Server-side** : `maximus-api /v1/prices` valide le tier premium AVANT cache/provider. Un client modifié qui bypass la UI reçoit 403.
+
+La double vérification est délibérée : le client est compromettable (l'app Tauri est ouverte au reverse-engineering), seul le serveur peut faire foi.
+
+### Consentement explicite (per-profile)
+
+- Stockage : `user_preferences.price_fetching_consent = {consented_at: , version: 1}`
+- **NE PAS seeder la clé.** Absence = jamais demandé. Le default doit être "non-décidé", pas "false".
+- Premier clic sur le bouton ↻ → modal de consentement → écriture de la clé après acceptation
+- **Permanence** : pas de re-consent automatique. Révocation explicite via toggle Settings (supprime la clé)
+- Stockage **per-profile** (table `user_preferences` est par-profil), pas global au système
+
+### Mode offline / fallback
+
+L'app **ne doit jamais bloquer la saisie d'un snapshot** parce que le price-fetching a échoué. La saisie manuelle de `unit_price` reste TOUJOURS disponible :
+
+| Erreur serveur | Comportement |
+|----------------|--------------|
+| 401 license expirée | Toast "Renouvelez votre abonnement" + champ manuel dispo |
+| 403 non-premium | Toast "Disponible avec abonnement Premium" + champ manuel dispo |
+| 404 symbole | Toast "Symbole introuvable — vérifiez l'orthographe" + champ manuel |
+| 429 rate limit | Toast "Limite atteinte — réessayez plus tard" + champ manuel |
+| Network error / 5xx | Toast "Service temporairement indisponible" + champ manuel |
+
+## Consequences
+
+### Positive
+
+- **L'IP de l'utilisateur n'est JAMAIS exposée à Yahoo / CoinGecko.** Le provider voit l'IP du VPS de Max — privacy-first préservée.
+- **Aucun symbole ne révèle de données personnelles.** "AAPL" ou "BTC" ne sont pas identifiants en soi ; corrélés à une license_id ils le redeviennent, c'est pourquoi le serveur ne logue jamais les deux ensemble.
+- **Cache mutualisé.** Si 500 utilisateurs premium demandent AAPL au 2026-03-15, c'est UN seul appel sortant côté maximus-api. Économise les rate limits ET réduit la surface d'exposition.
+- **Mode offline préservé.** L'app continue de fonctionner sans price-fetching — la saisie manuelle reste le chemin de secours.
+- **Justification commerciale.** Le price-fetching premium aligne le coût d'API tiers sur la révenue récurrente, sans dégrader l'expérience free-tier (qui reste 100 % local).
+- **Adapter pattern.** Si Yahoo casse (API non officielle), swap pour Alpha Vantage côté serveur sans changer le contrat client.
+
+### Negative / trade-offs
+
+- **Dépendance opérationnelle au VPS.** Si maximus-api est down, le price-fetching ne fonctionne pas — atténué par le fallback manuel toujours dispo.
+- **Surface serveur à maintenir.** Endpoint `/v1/prices` + cache + adapters + auth + rate limiting + observabilité (sans corrélation log).
+- **Charge financière sur Max.** Les tier free n'ont pas accès, donc les coûts d'API tiers sont absorbés par les abonnements premium ; le cache aide significativement.
+- **Implémentation BLOQUÉE.** L'Issue #143 ne peut shipper tant que `maximus-api` Phase 2 n'expose pas `/v1/prices` (dépendance externe : issues maximus-api `#49` license server core et `#136` Stripe webhooks).
+
+## Alternatives considered
+
+- **Appel direct client → provider.** Rejeté : viole le principe privacy-first (IP exposée + fingerprint headers).
+- **Tor / I2P intégré.** Rejeté : latence prohibitive (5-10 secondes par fetch), maintenance d'un client Tor embarqué dans Tauri, et certains providers bloquent les exits Tor.
+- **VPN tiers (Mullvad, etc.) configuré par l'utilisateur.** Rejeté : ne supprime pas le fingerprint headers, et "exiger l'utilisateur à configurer un VPN" est une régression UX inacceptable.
+- **Cache local sans serveur (chaque user a son propre cache).** Rejeté : pas de mutualisation, chaque user paie son propre rate limit, et le client doit toujours faire l'appel sortant initial (donc IP exposée).
+- **Saisie manuelle uniquement, pas de price-fetching du tout.** C'est le mode free-tier — fonctionnel mais friction élevée pour les utilisateurs avec un portefeuille actions/crypto significatif. Le proxy premium est le compromis qui justifie l'abonnement sans dégrader le free-tier.
+- **Endpoint `/v1/symbols/search` côté maximus-api** pour autocomplete. Reporté à v2 : l'autocomplete double la surface d'API et n'est pas critique. La saisie texte simple suffit au MVP.
+
+## References
+
+- Spec : [`spec-decisions-bilan.md`](../../spec-decisions-bilan.md), [`spec-plan-bilan.md`](../../spec-plan-bilan.md) (Issue #5 — Phase 5)
+- Spike : `~/claude-code/.spikes/bilan/code/price-fetching.md` (architecture, choix providers, consent flow)
+- Issue client (BLOCKED) : maximus/simpl-resultat #143
+- Issues maximus-api (externes, prerequisites) : `maximus-api#49` (license server core), `maximus-api#136` (Stripe webhooks)
+- Pattern auth : `src-tauri/src/commands/license_commands.rs` (`activation_path` + `activate_machine` — le token Bearer existe déjà)
+- Privacy frame : ce que `maximus-api` voit jamais ensemble = `(IP, license_id, symbol)`. Le proxy garantit que (IP) est cachée du provider et que (license_id, symbol) ne se retrouvent pas dans le même log.
diff --git a/docs/adr/0010-fk-restrict-balance-transfers.md b/docs/adr/0010-fk-restrict-balance-transfers.md
new file mode 100644
index 0000000..06113a3
--- /dev/null
+++ b/docs/adr/0010-fk-restrict-balance-transfers.md
@@ -0,0 +1,85 @@
+# ADR 0010 — `ON DELETE RESTRICT` sur `balance_account_transfers.transaction_id`
+
+- Status: Accepted
+- Date: 2026-04-25
+- Milestone: `overnight-2026-04-26-bilan` (Issues #138 → #145)
+
+## Context
+
+La table `balance_account_transfers` lie une `transaction` existante à un `balance_account` avec une direction (`'in'` = capital ajouté au compte, `'out'` = capital retiré). Cette table est l'input du calcul Modified Dietz (cf. [ADR 0008](0008-modified-dietz-pour-rendement.md)) qui sépare les **apports** des **gains réels** pour calculer la performance d'un compte d'investissement.
+
+La question structurante : que se passe-t-il si l'utilisateur supprime une transaction qui est liée à un transfert de bilan ?
+
+Trois politiques de FK sont possibles côté SQL :
+
+| Politique | Comportement | Intégrité historique | Friction utilisateur |
+|-----------|--------------|----------------------|----------------------|
+| `ON DELETE CASCADE` | Suppression de la transaction supprime aussi le transfert | ❌ Le rendement Modified Dietz d'une période passée change rétroactivement | ✅ Aucune friction : tout disparaît silencieusement |
+| `ON DELETE SET NULL` | Le transfert reste mais perd son `transaction_id` | ⚠ Le transfert devient "orphelin" : direction connue mais montant introuvable (les montants vivent dans `transactions.amount`) | ⚠ État partiellement valide |
+| **`ON DELETE RESTRICT`** | La suppression est bloquée par SQLite tant que des transferts pointent vers la transaction | ✅ Préservée : un rendement déjà calculé reste reproductible | ⚠ L'utilisateur doit délier explicitement avant suppression |
+
+Contraintes du contexte :
+- Modified Dietz produit un rendement **R** sur une période **[t1, t2]** à partir de `(V_début, V_fin, [(date, montant)])`. Si une `transaction` liée disparaît silencieusement (CASCADE), la fonction reste pure mais ses inputs changent — `R` calculé hier ≠ `R` calculé aujourd'hui sur la même période. C'est exactement l'antithèse de la reproductibilité financière.
+- Le calcul est déclenché à la demande (chargement de `BalanceAccountsTable`), il n'y a pas de cache server-side. Donc l'historique de "ce que le user a vu hier" n'existe pas : si les inputs bougent, le résultat affiché change sans que l'utilisateur sache pourquoi.
+- L'usage attendu de la suppression de transactions est rare et lié à des erreurs d'import (doublons, mauvaise source). Bloquer ce cas avec un message clair est acceptable.
+
+## Decision
+
+**Adopter `ON DELETE RESTRICT` sur `balance_account_transfers.transaction_id`** :
+
+```sql
+CREATE TABLE balance_account_transfers (
+ ...
+ transaction_id INTEGER NOT NULL,
+ ...
+ FOREIGN KEY (transaction_id) REFERENCES transactions(id) ON DELETE RESTRICT
+);
+```
+
+### UX correspondante
+
+La couche service `transactionService.ts` détecte l'erreur SQLite `FOREIGN KEY constraint failed` et la transforme en `TransactionLinkedToBalanceError` typée, qui porte la liste des comptes liés. La UI affiche alors :
+
+> **Cette transaction est liée au compte de bilan __.**
+> Pour la supprimer, déliez-la d'abord : ouvrez le compte → Lier transferts → décochez cette transaction.
+
+Avec un lien direct vers la `LinkTransfersModal` du compte concerné. L'utilisateur ne peut pas se retrouver bloqué : le chemin de déliaison est toujours dispo, à un clic du message d'erreur.
+
+Pour les chemins bulk (`deleteImportWithTransactions`, `deleteAllImportsWithTransactions`), une pré-vérification SELECT (`LIMIT 50`) liste les premiers transferts liés AVANT de tenter la suppression — l'utilisateur voit un message agrégé "X transactions de cet import sont liées à des comptes de bilan" plutôt qu'un raw FK error toast.
+
+### Direction CASCADE conservée pour `account_id`
+
+À noter : la même table a une autre FK, `account_id`, configurée en `ON DELETE CASCADE`. Si l'utilisateur supprime un compte de bilan, ses transferts disparaissent — c'est cohérent puisque les rendements de ce compte n'ont plus lieu d'être.
+
+L'asymétrie est délibérée :
+- `account_id` ON DELETE CASCADE : le compte de bilan est l'objet "principal" du domaine Bilan, sa suppression nettoie ses dépendances internes
+- `transaction_id` ON DELETE RESTRICT : la transaction est externe au domaine Bilan, sa suppression ne doit pas casser silencieusement les calculs
+
+## Consequences
+
+### Positive
+
+- **Reproductibilité Modified Dietz garantie.** Un rendement calculé sur une période passée ne peut pas changer à cause d'une suppression invisible côté `transactions`.
+- **Audit trail préservé.** L'utilisateur qui consulte un compte de bilan voit toujours les mêmes flux pour les mêmes périodes, peu importe quand il consulte.
+- **Erreur visible et actionnable.** L'utilisateur reçoit un message concret avec un chemin clair pour résoudre, plutôt qu'une suppression silencieuse qui invaliderait l'historique financier.
+- **Aligné avec la convention SQL existante du projet.** D'autres FK utilisent déjà `RESTRICT` quand l'intégrité est critique (cf. `balance_accounts.balance_category_id`, `balance_snapshot_lines.account_id`).
+
+### Negative / trade-offs
+
+- **Friction utilisateur** : forcer l'unlink explicite avant suppression ajoute 2 clics (ouvrir le compte → ouvrir LinkTransfersModal → décocher → revenir → supprimer). Acceptable car le cas est rare et le coût d'un rendement faux est élevé.
+- **Couplage UI ↔ erreur SQL** : `transactionService.ts` doit détecter le format d'erreur SQLite (`FOREIGN KEY constraint failed`) et le mapper sur `TransactionLinkedToBalanceError`. Si tauri-plugin-sql change le format du message d'erreur, le mapping casse silencieusement (mitigé par les tests d'intégration co-localisés dans `transactionService.test.ts`).
+- **Pré-vérification bulk a un coût** : un `SELECT ... LIMIT 50` sur `balance_account_transfers` à chaque suppression d'import. Négligeable en pratique (la table reste petite), mais à surveiller si un utilisateur a des dizaines de milliers de transferts.
+
+## Alternatives considered
+
+- **`ON DELETE CASCADE`.** Rejeté : trahit la promesse de reproductibilité du calcul Modified Dietz. Un rendement vu hier peut changer sans signal vers l'utilisateur.
+- **`ON DELETE SET NULL` + transferts orphelins.** Rejeté : laisse la base dans un état "valide mais incohérent". Le transfert sait sa direction mais a perdu son montant (qui vit dans `transactions.amount`). Le code Modified Dietz devrait alors filtrer les orphelins, et l'utilisateur ne saurait plus pourquoi son rendement a changé. Pire que CASCADE, qui au moins est explicite.
+- **Pas de FK du tout, juste un INTEGER orphelin possible.** Rejeté : retire toute garantie d'intégrité référentielle, et les calculs de rendement deviendraient une chasse aux pointeurs cassés.
+- **Soft-delete des transactions (`deleted_at` au lieu de DELETE)** pour préserver les données liées tout en cachant la transaction de l'UI. Rejeté pour l'instant : les transactions n'ont pas de soft-delete dans le schéma actuel et l'introduire ouvrirait un chantier transversal (toutes les requêtes de transactions devraient filtrer `WHERE deleted_at IS NULL`). À reconsidérer si plusieurs domaines en font la demande.
+
+## References
+
+- Implémentation : `src-tauri/src/database/balance_schema.sql` (FK definition), `src/services/transactionService.ts` (`TransactionLinkedToBalanceError` mapping)
+- Tests : `src/services/transactionService.test.ts` (mapping FK error → typed error), `src/__integration__/balance-flow.test.ts` (lien + tentative de suppression bloquée)
+- Spec : [`spec-decisions-bilan.md`](../../spec-decisions-bilan.md) — décision "FK `balance_account_transfers.transaction_id` : `ON DELETE RESTRICT` + UI force unlink avec message clair"
+- ADR liée : [0008 — Modified Dietz pour le calcul du rendement](0008-modified-dietz-pour-rendement.md)
diff --git a/docs/architecture.md b/docs/architecture.md
index 93166c5..b312489 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,6 +1,6 @@
# Architecture technique — Simpl'Résultat
-> Document mis à jour le 2026-04-13 — Version 0.7.3
+> Document mis à jour le 2026-04-25 — Version 0.8.x (Bilan)
## Stack technique
@@ -28,6 +28,7 @@ simpl-resultat/
├── src/ # Frontend React/TypeScript
│ ├── components/ # 58 composants organisés par domaine
│ │ ├── adjustments/ # 3 composants
+│ │ ├── balance/ # 7 composants Bilan (AccountForm, BalanceAccountsTable, BalanceEvolutionChart, BalanceOverviewCard, LinkTransfersModal, SnapshotEditor, SnapshotLineRow)
│ │ ├── budget/ # 5 composants
│ │ ├── categories/ # 5 composants
│ │ ├── dashboard/ # 2 composants
@@ -72,7 +73,7 @@ simpl-resultat/
## Base de données
-### Tables (13)
+### Tables (18)
| Table | Description |
|-------|-------------|
@@ -89,10 +90,36 @@ simpl-resultat/
| `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) |
+| `balance_categories` | Taxonomie des types d'actifs (cash, TFSA, RRSP, fund, stock, crypto, other) — `kind ∈ {simple, priced}`, 7 seedées (`is_seed = 1`) |
+| `balance_accounts` | Comptes de bilan (rattachés à une catégorie). `currency` hardcodée à `CAD` au MVP via CHECK. `archived_at` pour soft-delete |
+| `balance_snapshots` | Snapshots datés (`snapshot_date` UNIQUE) — éditer = mettre à jour les lignes, pas dupliquer |
+| `balance_snapshot_lines` | Une ligne par `(snapshot, compte)`. Stockage denormalisé : pour `simple` `value` seul, pour `priced` `quantity + unit_price + value`. CHECK kind invariants côté SQL |
+| `balance_account_transfers` | Liaison `transactions ↔ balance_accounts` avec `direction ∈ {in, out}`. Utilisée par le calcul Modified Dietz pour séparer apports et gains |
-### Index (9)
+### Index (16)
-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).
+Index existants (9) : `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).
+
+Index Bilan (7, ajoutés en migration v9) :
+- `idx_balance_accounts_category` (FK lookup catégorie → comptes)
+- `idx_balance_accounts_active` partiel `WHERE is_active = 1` (filtre liste active)
+- `idx_balance_snapshot_lines_snapshot` (chargement d'un snapshot)
+- `idx_balance_snapshot_lines_account` (historique par compte)
+- `idx_balance_account_transfers_account` (cash flows Modified Dietz par compte)
+- `idx_balance_account_transfers_transaction` (lookup icône d'attribution dans `TransactionTable`)
+- `idx_balance_snapshots_date` (sélecteur de période + agrégation chronologique)
+
+### Invariants Bilan (CHECK + FK)
+
+- `balance_categories.kind` ∈ `('simple','priced')`
+- `balance_accounts.currency = 'CAD'` (verrou MVP — v2 lèvera ce CHECK avec table de taux)
+- `balance_snapshot_lines` : `(quantity, unit_price)` doivent être tous deux NULL (kind simple) OU tous deux NOT NULL (kind priced)
+- `balance_account_transfers.direction` ∈ `('in','out')` ; UNIQUE `(transaction_id, account_id)` (une transaction ne peut pas être liée deux fois au même compte)
+- FK `balance_accounts.balance_category_id` → `balance_categories(id)` `ON DELETE RESTRICT` (empêche suppression de catégorie avec comptes liés)
+- FK `balance_snapshot_lines.snapshot_id` → `balance_snapshots(id)` `ON DELETE CASCADE` (supprimer un snapshot supprime ses lignes)
+- FK `balance_snapshot_lines.account_id` → `balance_accounts(id)` `ON DELETE RESTRICT` (préserve l'historique)
+- FK `balance_account_transfers.account_id` → `balance_accounts(id)` `ON DELETE CASCADE`
+- FK `balance_account_transfers.transaction_id` → `transactions(id)` `ON DELETE RESTRICT` — décision structurante pour la reproductibilité Modified Dietz, voir [ADR 0010](adr/0010-fk-restrict-balance-transfers.md)
## Système de migrations
@@ -107,17 +134,19 @@ Les migrations sont définies inline dans `src-tauri/src/lib.rs` via `tauri_plug
| 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) |
+| 8 | v8 | Migration de catégories (cf. release 0.8.x) |
+| 9 | v9 | Schéma Bilan : 5 tables + 7 index + seed des 7 catégories standard (cash, TFSA, RRSP, fund, stock, crypto, other) |
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)
+## Services TypeScript (18)
| 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 |
+| `transactionService.ts` | CRUD et filtrage des transactions ; détection d'erreurs FK RESTRICT pour transactions liées à un compte de bilan (typed `TransactionLinkedToBalanceError`) |
| `importSourceService.ts` | Configuration des sources d'import |
| `importedFileService.ts` | Suivi des fichiers importés |
| `importConfigTemplateService.ts` | Modèles de configuration d'import |
@@ -131,8 +160,20 @@ Pour les **nouveaux profils**, le fichier `consolidated_schema.sql` contient le
| `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_*) |
+| `balance.service.ts` | Domaine Bilan — service unique avec 4 sections logiques (voir détail ci-dessous) |
-## Hooks (14)
+### Service Bilan — `balance.service.ts`
+
+Un seul service par convention projet (1 service par domaine, splitter seulement > ~400 lignes). Quatre sections logiques distinctes :
+
+1. **CRUD catégories + comptes** — `listBalanceCategories`, `createBalanceCategory`, `updateBalanceCategory`, `archiveBalanceCategory` (refus si comptes liés via FK RESTRICT, refus si `is_seed = 1`), `listBalanceAccounts`, `createBalanceAccount`, `updateBalanceAccount`, `archiveBalanceAccount`. Le service garde une `BalanceServiceError` typée (`BalanceErrorCode`) pour permettre à la UI d'afficher des messages i18n distincts (`currency_unsupported`, `category_seed_protected`, `category_has_accounts`, etc.).
+2. **Snapshots + lines** — `listBalanceSnapshots`, `getBalanceSnapshotByDate`, `upsertSnapshot` (création + édition par date), `upsertSnapshotLines` (rewrite-all : DELETE WHERE snapshot_id puis INSERT par ligne — choix simple pour < 20 comptes/snapshot), `deleteSnapshot`, helper `validateLineKindInvariants` exporté pour les tests (kind invariants TS en complément du CHECK SQL ; tolérance `PRICED_VALUE_TOLERANCE = 0.01` pour le match `value ≈ quantity × unit_price`).
+3. **Returns + transfers** — `linkTransfer`, `unlinkTransfer`, `listAccountTransfers`, `listAllLinkedTransfersForTooltip` (un coup pour la `Map.has(txId)` consommée par l'icône d'attribution dans `TransactionTable`), `computeAccountReturn` (wrapper sur la commande Tauri `compute_account_return` qui lit `db_filename` du profil actif via `loadProfiles()`).
+4. **Prices** — *(Phase 5, livraison reportée à l'Issue #143)*. La forme prévue : `fetchPrice(symbol, date)` invoquant `fetch_price` (Tauri), avec rate-limit client (1/2s), backoff exponentiel et dedup in-flight. Voir [ADR 0009](adr/0009-proxy-price-fetching-via-maximus-api.md) pour l'architecture proxy.
+
+Le CRUD passe par `getDb()` + `tauri-plugin-sql` direct, **jamais** via une commande Tauri — convention projet. Les commandes Rust sont réservées au filesystem, OAuth, license, profils, feedback et au seul calcul Modified Dietz (qui a besoin d'arithmétique de dates `chrono`).
+
+## Hooks (17+)
Chaque hook encapsule la logique d'état via `useReducer` :
@@ -152,13 +193,16 @@ Chaque hook encapsule la logique d'état via `useReducer` :
| `useCompare` | Rapport Comparables (mode `actual`/`budget`, sous-toggle MoM ↔ YoY, mois de référence explicite avec wrap-around janvier) |
| `useCategoryZoom` | Rapport Zoom catégorie avec rollup sous-catégories |
| `useCartes` | Rapport Cartes (snapshot KPI + sparklines + top movers + budget + saisonnalité via `getCartesSnapshot`) |
+| `useBalanceAccounts` | Bilan — état de la page `/balance/accounts` : CRUD comptes ET catégories (un seul hook pour les deux onglets, aligné sur la convention "1 hook par page") |
+| `useSnapshotEditor` | Bilan — cycle de vie d'un snapshot unique (`/balance/snapshot`) : valeurs simple (string) + valeurs priced (`{quantity, unit_price}` strings), prefill depuis snapshot précédent, save (rewrite-all), delete avec double-confirmation par re-saisie de la date |
+| `useBalanceOverview` | Bilan — page `/balance` : sélecteur de période (`3M / 6M / 1A / 3A / Tout`), série temporelle agrégée, mode chart (`line` / `stacked`), tableau des comptes avec valeurs courantes et Δ% sur la période. Les rendements multi-horizons sont chargés *lazily* dans `BalanceAccountsTable` (un appel `compute_account_return` par cellule) |
| `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)
+## Commandes Tauri (36)
### `fs_commands.rs` — Système de fichiers (6)
@@ -230,6 +274,14 @@ Module privé appelé uniquement par `auth_commands.rs` et `license_commands.rs`
- 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
+### `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).
+
+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).
+
+**À venir Phase 5** (Issue #143, BLOCKED par maximus-api Phase 2) : commande `fetch_price(symbol, date)` pour le price-fetching premium via proxy maximus-api. L'architecture est documentée dans l'ADR 0009 ; la livraison est différée jusqu'à ce que le serveur de licences (`maximus-api`) expose l'endpoint `GET /v1/prices`.
+
## Plugins Tauri
Ordre d'initialisation dans `lib.rs` (certains plugins ont des contraintes d'ordre) :
@@ -291,6 +343,9 @@ Le routing est défini dans `App.tsx`. Toutes les pages sont englobées par `App
| `/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é |
+| `/balance` | `BalancePage` | Bilan — vue d'ensemble : carte "Aujourd'hui" + Δ% + avertissement bilan pas à jour > 60j, graphique d'évolution (toggle ligne / aire empilée par catégorie), tableau des comptes avec rendements multi-horizons (3M / 1A / depuis création — Modified Dietz) côte-à-côte avec rendement non-ajusté |
+| `/balance/snapshot` | `SnapshotEditPage` | Saisie / édition d'un snapshot daté. Mode `?date=today` (création) ou `?date=YYYY-MM-DD` (édition, date immutable). Lignes groupées par catégorie : `simple` = champ valeur, `priced` = `quantity` × `unit_price` (`value` calculé read-only). Bouton "Pré-remplir depuis le snapshot précédent". Suppression à double-confirmation par re-saisie de la date |
+| `/balance/accounts` | `AccountsPage` | CRUD comptes + catégories de bilan (deux onglets). Catégories seedées (`is_seed = 1`) renommables mais non-supprimables ; refus de suppression d'une catégorie avec comptes liés (FK RESTRICT) |
| `/settings` | `SettingsPage` | Paramètres |
| `/docs` | `DocsPage` | Documentation in-app |
| `/changelog` | `ChangelogPage` | Historique des versions (bilingue FR/EN) |
diff --git a/docs/guide-utilisateur.md b/docs/guide-utilisateur.md
index 589f11d..5c3e9a6 100644
--- a/docs/guide-utilisateur.md
+++ b/docs/guide-utilisateur.md
@@ -355,7 +355,74 @@ L'application est atomique : soit toutes les transactions cochées sont recatég
---
-## 10. Paramètres
+## 10. Bilan
+
+Le **Bilan** est une vue patrimoniale : vous saisissez périodiquement un *snapshot* daté de l'ensemble de vos comptes (encaisse, REER, CELI, fonds, actions, crypto, autres), vous suivez leur évolution dans le temps, et vous calculez le **vrai rendement** de chaque compte d'investissement en liant les transferts (apports / retraits) aux comptes correspondants.
+
+Trois pages composent le module Bilan :
+- `/balance` — vue d'ensemble (graphique + tableau des comptes)
+- `/balance/snapshot` — saisie / édition d'un snapshot daté
+- `/balance/accounts` — CRUD des comptes et catégories
+
+L'entrée **Bilan** dans la barre latérale (icône portefeuille) donne accès à `/balance` ; les deux autres pages s'ouvrent depuis là.
+
+### Fonctionnalités
+
+- 7 catégories standard pré-installées : Encaisse, CELI, REER, Fonds, Actions, Crypto, Autres — renommables, non-supprimables
+- Création de catégories personnalisées (ex. FERR, RPDB) avec choix `simple` (montant direct) ou `priced` (quantité × prix unitaire)
+- Comptes par catégorie : nom, symbole optionnel, devise (CAD au MVP), notes
+- Snapshots datés avec contrainte UNIQUE par date — éditer = revenir sur la même date, jamais dupliquer
+- Saisie groupée par catégorie ; pour les catégories `priced`, le `value` est calculé automatiquement (`quantity × unit_price`)
+- Bouton **Pré-remplir depuis le snapshot précédent** : copie les valeurs simples + les quantités priced (vous remplissez juste les nouveaux prix)
+- Liaison de transactions existantes à un compte de bilan (modal avec filtres par période / catégorie / recherche, sens auto-proposé selon le signe)
+- Icône d'attribution dans la page Transactions pour les transactions liées à un transfert
+- Graphique d'évolution du bilan (mode courbe simple ou aire empilée par catégorie) avec marqueurs verticaux pour les transferts taggés (vert = in, rouge = out)
+- Tableau des comptes avec **3 colonnes de rendement Modified Dietz** (3 mois / 1 an / depuis création) + colonne rendement non-ajusté côte-à-côte
+- Avertissement si le dernier snapshot remonte à plus de 60 jours
+- Soft-delete des comptes (`Archiver`) : masqués des nouveaux snapshots, conservés dans l'historique
+- Suppression d'un snapshot avec double-confirmation (re-saisie de la date)
+- Privacy-first : tout est local, aucun appel sortant au MVP
+
+### Comment faire
+
+1. Allez dans `/balance/accounts` → onglet Catégories pour créer si besoin une catégorie supplémentaire (ex. "FERR" en `simple`, ou "Stocks Wealthsimple" en `priced`)
+2. Allez dans l'onglet Comptes pour créer chaque compte (ex. "TFSA Tangerine" rattaché à CELI, "BTC Ledger" rattaché à Crypto avec symbole `BTC`)
+3. Cliquez **+ Nouveau snapshot** depuis `/balance` pour ouvrir `/balance/snapshot` à la date du jour
+4. Remplissez les valeurs par compte (groupées par catégorie). Pour les comptes priced, saisissez la quantité et le prix unitaire — la valeur est calculée
+5. Enregistrez. Le graphique sur `/balance` s'actualise immédiatement
+6. Pour calculer le rendement réel d'un compte d'investissement, ouvrez le menu actions du compte → **Lier transferts** → cochez les transactions qui correspondent à des apports / retraits (un dépôt CELI, un achat d'actions, etc.). Le sens (in/out) est proposé automatiquement selon le signe de la transaction
+7. Le tableau des comptes affiche maintenant les rendements Modified Dietz sur 3M / 1A / depuis création. Le rendement non-ajusté à droite vous permet de comparer "valeur du compte" et "vraie performance"
+8. Pour éditer un snapshot existant, cliquez sur son point dans le graphique ou utilisez le sélecteur de date — la page s'ouvre en mode édition (la date est immutable)
+9. Pour supprimer un snapshot, cliquez **Supprimer** dans son éditeur et re-saisissez la date pour confirmer
+
+### Lecture des rendements multi-horizons
+
+- **3 mois** : performance courte, sensible aux mouvements récents
+- **1 an** : horizon de référence pour la plupart des décisions d'allocation
+- **Depuis création** : performance totale du compte depuis le premier snapshot
+- **Non-ajusté (côte-à-côte)** : `(V_fin − V_début) / V_début` brut, sans soustraction des apports — utile pour voir la croissance totale (gains + apports). La différence entre les deux colonnes vous montre la part qui vient des apports plutôt que de la performance
+
+Avertissements affichés :
+- *Période partielle* — un snapshot manque au début ou à la fin de la période
+- *Aucun transfert lié* — le rendement est calculé sans apports identifiés (équivaut au non-ajusté)
+- *Performance non significative* — le compte a été vidé puis rechargé, le calcul Modified Dietz produit un résultat instable
+
+### Que faire si je supprime une transaction liée ?
+
+C'est intentionnellement bloqué : si vous tentez de supprimer une transaction qui est liée à un compte de bilan, vous voyez le message **"Cette transaction est liée au compte de bilan __"** avec un lien direct vers le compte. Ouvrez le compte → Lier transferts → décochez la transaction → revenez la supprimer. Cette friction préserve la reproductibilité de vos rendements passés (un rendement calculé hier ne peut pas changer aujourd'hui à cause d'une suppression silencieuse).
+
+### Astuces
+
+- Saisissez vos snapshots à un rythme régulier (mensuel ou trimestriel) — la qualité des rendements dépend directement de la régularité
+- Utilisez le bouton **Pré-remplir** : ça copie tout, vous mettez juste à jour ce qui a changé
+- Le mode **graphique empilé par catégorie** raconte une histoire différente du mode ligne : il montre la composition de votre patrimoine, pas seulement son total
+- Les marqueurs verticaux du graphique (transferts taggés) aident à lire les sauts de valeur — un saut suivi d'un marqueur vert n'est pas une "performance", c'est juste un dépôt
+- L'avertissement "bilan pas à jour" apparaît si votre dernier snapshot remonte à plus de 60 jours — c'est le signe qu'il est temps d'en saisir un nouveau
+- (À venir Phase 5) **Récupération automatique des prix** pour les comptes Actions / Crypto via un proxy privé (premium-only). Le service interroge un serveur Maximus dédié qui anonymise votre requête (votre IP n'est jamais exposée à Yahoo / CoinGecko). La saisie manuelle reste toujours disponible.
+
+---
+
+## 11. Paramètres
Configurez les préférences de l'application, vérifiez les mises à jour, accédez au guide utilisateur et gérez vos données avec les outils d'export/import.
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 6012db9..6678ee7 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -901,6 +901,44 @@
"Seasonality, top movers, and budget adherence stay monthly even when the toggle is set to YTD — only the 4 KPI numbers change"
]
},
+ "balance": {
+ "title": "Balance Sheet",
+ "overview": "The Balance Sheet is a net-worth view: you periodically enter a dated snapshot of all your accounts (cash, RRSP, TFSA, funds, stocks, crypto, other), track their evolution over time, and compute the true return of each investment account by linking transfers (deposits/withdrawals) to the matching accounts.",
+ "features": [
+ "7 standard categories pre-installed (Cash, TFSA, RRSP, Fund, Stock, Crypto, Other) — renameable, non-deletable",
+ "Custom category creation with simple (direct amount) or priced (quantity × unit price) kind",
+ "Accounts per category: name, optional symbol, currency (CAD at MVP), notes",
+ "Dated snapshots with a UNIQUE constraint per date — editing means revisiting the same date, never duplicating",
+ "\"Prefill from previous snapshot\" button: copies simple values + priced quantities",
+ "Linking existing transactions to a balance account (modal with filters and auto-suggested direction)",
+ "Attribution icon in the Transactions page for transactions linked to a transfer",
+ "Evolution chart with line or stacked-area-by-category mode + vertical markers for tagged transfers (green = in, red = out)",
+ "Accounts table with 3 Modified Dietz return columns (3M / 1Y / since inception) + side-by-side unadjusted return column",
+ "Warning if the latest snapshot is more than 60 days old",
+ "Soft-delete of accounts (Archive) — hidden from new snapshots, preserved in history",
+ "Snapshot deletion with double-confirmation by retyping the date",
+ "Privacy-first: everything stays local, no outbound calls at MVP"
+ ],
+ "steps": [
+ "Go to /balance/accounts → Categories tab to create an extra category if needed (RRIF as simple, or Stocks Wealthsimple as priced)",
+ "Go to the Accounts tab to create each account (TFSA Tangerine under TFSA, BTC Ledger under Crypto with symbol BTC)",
+ "Click \"+ New snapshot\" from /balance to open /balance/snapshot at today's date",
+ "Fill in values per account (grouped by category). For priced accounts, enter quantity and unit price — value is computed",
+ "Save. The chart on /balance refreshes immediately",
+ "To compute the real return of an investment account, open the actions menu → \"Link transfers\" → check the transactions matching deposits/withdrawals — direction (in/out) is auto-proposed",
+ "The accounts table now shows Modified Dietz returns over 3M / 1Y / since inception, side-by-side with the unadjusted return",
+ "To edit an existing snapshot, click its point on the chart or use the date picker — the page opens in edit mode (date is immutable)",
+ "To delete a snapshot, click \"Delete\" in its editor and retype the date to confirm"
+ ],
+ "tips": [
+ "Take snapshots on a regular cadence (monthly or quarterly) — return quality depends on regularity",
+ "The unadjusted return on the right shows \"account value\" vs \"true performance\": the difference comes from contributions, not from performance",
+ "Vertical chart markers help you read value jumps: a jump followed by a green marker isn't \"performance\", it's a deposit",
+ "If you try to delete a transaction linked to a balance account, the app asks you to unlink it first — this friction preserves the reproducibility of past returns",
+ "The \"balance out of date\" warning appears if your latest snapshot is more than 60 days old",
+ "(Coming in Phase 5) Automatic price fetching for Stocks/Crypto via a private proxy (premium-only) that anonymizes your request — manual entry remains always available"
+ ]
+ },
"settings": {
"title": "Settings",
"overview": "Configure app preferences, check for updates, access the user guide, and manage your data with export/import tools.",
diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json
index 3a95469..eef8cc8 100644
--- a/src/i18n/locales/fr.json
+++ b/src/i18n/locales/fr.json
@@ -901,6 +901,44 @@
"La saisonnalité, les top mouvements et l'adhésion budgétaire restent mensuels même quand le toggle est sur YTD — seuls les 4 chiffres KPI changent"
]
},
+ "balance": {
+ "title": "Bilan",
+ "overview": "Le Bilan est une vue patrimoniale : vous saisissez périodiquement un snapshot daté de l'ensemble de vos comptes (encaisse, REER, CELI, fonds, actions, crypto, autres), suivez leur évolution dans le temps et calculez le vrai rendement de chaque compte d'investissement en liant les transferts (apports/retraits) aux comptes correspondants.",
+ "features": [
+ "7 catégories standard pré-installées (Encaisse, CELI, REER, Fonds, Actions, Crypto, Autres) — renommables, non-supprimables",
+ "Création de catégories personnalisées avec choix simple (montant direct) ou priced (quantité × prix unitaire)",
+ "Comptes par catégorie : nom, symbole optionnel, devise (CAD au MVP), notes",
+ "Snapshots datés avec contrainte UNIQUE par date — éditer = revenir sur la même date, jamais dupliquer",
+ "Bouton « Pré-remplir depuis le snapshot précédent » : copie les valeurs simples + les quantités priced",
+ "Liaison de transactions existantes à un compte de bilan (modal avec filtres et sens auto-proposé)",
+ "Icône d'attribution dans la page Transactions pour les transactions liées à un transfert",
+ "Graphique d'évolution avec mode courbe ou aire empilée par catégorie + marqueurs verticaux pour les transferts (vert = in, rouge = out)",
+ "Tableau des comptes avec 3 colonnes de rendement Modified Dietz (3M / 1A / depuis création) + colonne rendement non-ajusté côte-à-côte",
+ "Avertissement si le dernier snapshot remonte à plus de 60 jours",
+ "Soft-delete des comptes (Archiver) — masqués des nouveaux snapshots, conservés dans l'historique",
+ "Suppression d'un snapshot avec double-confirmation par re-saisie de la date",
+ "Privacy-first : tout est local, aucun appel sortant au MVP"
+ ],
+ "steps": [
+ "Allez dans /balance/accounts → onglet Catégories pour créer si besoin une catégorie supplémentaire (FERR en simple, ou Stocks Wealthsimple en priced)",
+ "Allez dans l'onglet Comptes pour créer chaque compte (TFSA Tangerine rattaché à CELI, BTC Ledger rattaché à Crypto avec symbole BTC)",
+ "Cliquez « + Nouveau snapshot » depuis /balance pour ouvrir /balance/snapshot à la date du jour",
+ "Remplissez les valeurs par compte (groupées par catégorie). Pour les comptes priced, saisissez la quantité et le prix unitaire — la valeur est calculée",
+ "Enregistrez. Le graphique sur /balance s'actualise immédiatement",
+ "Pour calculer le rendement réel d'un compte d'investissement, ouvrez le menu actions → « Lier transferts » → cochez les transactions qui correspondent à des apports/retraits — le sens (in/out) est proposé automatiquement",
+ "Le tableau des comptes affiche maintenant les rendements Modified Dietz sur 3M / 1A / depuis création, avec le rendement non-ajusté côte-à-côte",
+ "Pour éditer un snapshot existant, cliquez sur son point dans le graphique ou utilisez le sélecteur de date — la page s'ouvre en mode édition (la date est immutable)",
+ "Pour supprimer un snapshot, cliquez « Supprimer » dans son éditeur et re-saisissez la date pour confirmer"
+ ],
+ "tips": [
+ "Saisissez vos snapshots à un rythme régulier (mensuel ou trimestriel) — la qualité des rendements dépend de la régularité",
+ "Le rendement non-ajusté à droite vous permet de voir « valeur du compte » vs « vraie performance » : la différence vient des apports, pas de la performance",
+ "Les marqueurs verticaux du graphique aident à lire les sauts de valeur : un saut suivi d'un marqueur vert n'est pas une « performance », c'est un dépôt",
+ "Si vous tentez de supprimer une transaction liée à un compte de bilan, l'app vous demande de la délier d'abord — cette friction préserve la reproductibilité de vos rendements passés",
+ "L'avertissement « bilan pas à jour » apparaît si votre dernier snapshot remonte à plus de 60 jours",
+ "(À venir Phase 5) Récupération automatique des prix pour Actions/Crypto via un proxy privé (premium-only) qui anonymise votre requête — la saisie manuelle reste toujours disponible"
+ ]
+ },
"settings": {
"title": "Paramètres",
"overview": "Configurez les préférences de l'application, vérifiez les mises à jour, accédez au guide utilisateur et gérez vos données avec les outils d'export/import.",
diff --git a/src/pages/DocsPage.tsx b/src/pages/DocsPage.tsx
index 210c3b5..c7a0238 100644
--- a/src/pages/DocsPage.tsx
+++ b/src/pages/DocsPage.tsx
@@ -10,6 +10,7 @@ import {
SlidersHorizontal,
PiggyBank,
BarChart3,
+ Wallet,
Settings,
ArrowLeft,
Lightbulb,
@@ -29,6 +30,7 @@ const SECTIONS = [
{ key: "adjustments", icon: SlidersHorizontal },
{ key: "budget", icon: PiggyBank },
{ key: "reports", icon: BarChart3 },
+ { key: "balance", icon: Wallet },
{ key: "settings", icon: Settings },
] as const;