# #85 — Refonte architecturale : découpage du monolithe & persistance d'état (phase 2) > **Statut :** livré (T1→T10) — `backend/main.py` 4 827 → ~750 lignes, 14 routers, > persistance partielle (stores verrouillés + rate-limit SQLite optionnel). > Méthode : tranches à impact minimal, comportement inchangé, un domaine par > commit, suite complète verte à chaque commit (1320 passed / 6 skipped). ## 1. Découpage du monolithe (T1→T9, comportement inchangé) Chaque tranche déplace un domaine vers `backend/routers/` (handlers verbatim, mêmes chemins/modèles/auth/tags OpenAPI), les modèles vers `backend/schemas.py`, et ne committe que sur suite verte + `test_version` vert. | Tranche | Domaine | Nouveau module | Version | |---|---|---|---| | T1 | health (`/api/health*`) | `routers/health.py` (+ `HealthResponse` → schemas) | 2.27.2 | | T2 | webhooks CRUD | `routers/webhooks.py` | 2.27.3 | | T3 | sharing (`/api/share*`, `/s/*`) | `routers/sharing.py` | 2.27.4 | | T4 | backups (9 routes) | `routers/backups.py` (+ `Diff/Restore*` → schemas, `backend/sse.py`) | 2.27.5 | | T5 | search (11 routes) | `routers/search.py` (+ modèles → schemas, `backend/search_executor.py`) | 2.27.6 | | T6a | lecture fichiers | `routers/files_read.py` (+ modèles, `routers/helpers.py`) | 2.27.7 | | T6b | mutations fichiers/dossiers | `routers/files_write.py` (+ 15 modèles → schemas) | 2.27.8 | | T6c | media/pdf/export/guide | `routers/files_media.py` (Range helper → `helpers.py`) | 2.27.9 | | T7 | config (12 routes) | `routers/config.py` (`_FALLBACK_MODELS` déplacé) | 2.27.10 | | T8 | vaults + history + conflicts (13 routes) | `routers/vaults.py`, `history.py`, `conflicts.py` (+ `backend/watcher_state.py`) | 2.27.11 | | T9 | realtime + render | `routers/realtime.py` (SSE + collab WS), `backend/render.py` | 2.27.12 | `main.py` ne contient plus que l'assemblage : lifespan, middlewares, montage des routers, racine `/api`, statique/SPA, 4 cales de compatibilité testées (`_resolve_safe_path`, `_backup_file`, `_check_vault_writable`, `_get_backup_dir`). Correctifs au passage : décorateur orphelin `/s/{token}` (double-enregistrement de `/api/conflicts`), tag OpenAPI `media` inexistant (assignation par chemin conservée), tests statiques frontend réalignés (`image-viewer`, `media-viewer`), tests repointés vers les modules canoniques (`test_ai_models`, `test_api_main`). ## 2. Persistance d'état (T10) | État | Avant | Après | |---|---|---| | JTI révoqués (`revoked_tokens.json`) | persisté, **sans verrou** | `RLock` (load/save/revoke/check) | | `shares.json` | persisté, **sans verrou** | `RLock` (4 mutateurs) | | `webhooks.json` + secrets | persistés, **sans verrou** | `RLock` (create/update/delete/secrets) | | `api_keys.json` (tool-secrets) | persisté, **sans verrou** | `RLock` (set/delete) | | Rate-limit auth | mémoire, mono-process | **inchangé par défaut** + option `OBSIGATE_RATELIMIT_DB` (SQLite WAL : mêmes fenêtres/budgets, partagé multi-workers, survit au redémarrage) | | Index de recherche | mémoire, rebuild au démarrage | **conservé** (voir §3) | | `users.json`, `api_tokens.json`, `vault_settings.json` | déjà verrouillés (BUG-029, #107) | inchangé | Tests : `tests/test_store_locks.py` (4 — concurrence threads, pertes prouvées sans verrou : 25/200 partages), `tests/test_ratelimit_store.py` (7 — sémantique SQLite identique, persistance, concurrence 200/200). Déjà existants et vérifiés (pas de code) : verrous `threading` + `asyncio` de l'indexeur (`_index_lock`, `_async_index_lock`), contrat central des outils IA — `backend/tools/registry.py` couvre déjà permissions (`requires_vault`, `require_destructive_allowed`), quotas (`check_and_record` par outil) et redaction (`redact_payload`) pour les 35 outils enregistrés via `@tool(`. ## 3. Décisions assumées (non fait, et pourquoi) - **Index non persisté sur disque.** Le rebuild différentiel (#86 : réutilise les entrées inchangées `size` + `mtime`) rend le démarrage rapide ; un snapshot introduirait des risques de staleness/drift de format sans gain mesuré. Réévaluer si le démarrage devient lent (vaults 50k+ fichiers). - **Redis exclu.** SQLite WAL couvre le multi-workers mono-hôte sans nouvelle infra ; Redis reste l'option multi-nœuds documentée (cf. `ratelimit.py`). - **`.gitignore` (`_*.py` ignore les `__init__.py`).** Contourné par `git add -f` comme les packages existants ; assainir la règle à part. - Noms en `_` conservés (`backend/render.py`, stores) : déplacement verbatim, zéro churn d'appels. ## 4. Reste connu (hors #85) - CSP `unsafe-inline` (migration nonce, BUG-034 partiel) et `Secure` cookies → #87. - `main.py` (~750 lignes) : lifespan, middlewares, statique/SPA — cible d'extraction ultérieure si besoin, non bloquant.