docs(coordination): installateur bêta v11 + 4 sous-plans agents v11.5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
from: claude
|
||||||
|
to: dom
|
||||||
|
date: 2026-06-05T18:20:00+02:00
|
||||||
|
topic: package-beta-installateur-maj
|
||||||
|
status: done
|
||||||
|
references:
|
||||||
|
- file: docs/coordination/inbox/for-dom/2026-06-05_claude_pack-beta-build-report.md
|
||||||
|
- decision: docs/coordination/decisions/2026-06-05_dom_d16-test-windows-avant-diffusion.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rapport package bêta — MISE À JOUR (installateur ajouté)
|
||||||
|
|
||||||
|
Suite à ton GO, l'installateur Inno Setup a été produit (étapes D-16 §1-4).
|
||||||
|
|
||||||
|
## Installateur produit ✅
|
||||||
|
- Inno Setup **déjà présent** (chemin user `%LOCALAPPDATA%\Programs\Inno Setup 6\`) —
|
||||||
|
pas besoin de l'installer. (Mon check initial cherchait dans `Program Files`, d'où
|
||||||
|
le « absent » précédent.)
|
||||||
|
- Build via `build_windows_installer_only.ps1` (depuis l'EXE existant, **pas de
|
||||||
|
rebuild**) — `INSTALLER_EXIT_CODE=0`.
|
||||||
|
- **`release\Anonymisation-Setup.exe`** — **729 576 700 octets (695.8 Mo)**, langue FR.
|
||||||
|
- Non signé (conforme D-3).
|
||||||
|
|
||||||
|
## Artefacts finaux (`192.168.1.11:…\release\`)
|
||||||
|
| Fichier | Taille | SHA-256 |
|
||||||
|
|---|---|---|
|
||||||
|
| `Anonymisation.exe` | 696.7 Mo | `80A740DA…F629DF9` |
|
||||||
|
| `Anonymisation-Windows.zip` | 728 Mo | `E22F8264…A8A2` |
|
||||||
|
| `Anonymisation-Setup.exe` | 695.8 Mo | `12D9D6A8…663DC` |
|
||||||
|
|
||||||
|
(Manifeste complet : `release\SHA256SUMS.txt`.)
|
||||||
|
|
||||||
|
Tout est issu de `feature/q1-quarantine-mvp @ 15f73f8` (leak 100/100, validé GO).
|
||||||
|
|
||||||
|
## ⏸️ Reste : diffusion OwnCloud — j'ai besoin du canal
|
||||||
|
Conformément à D-16, je n'ai **rien uploadé**. Tu m'as donné le GO mais pas le
|
||||||
|
**dossier/URL OwnCloud** de dépôt. Dis-moi où déposer (ZIP seul ? ou ZIP + Setup +
|
||||||
|
SHA256SUMS ?) et je m'en charge. Sinon le pack reste prêt en local.
|
||||||
|
|
||||||
|
## Recommandation diffusion
|
||||||
|
Pour le bêta-testeur, je suggère de livrer **soit le ZIP soit le Setup**, pas les deux
|
||||||
|
(évite la confusion), + `SHA256SUMS.txt`. Le ZIP (EXE portable + docs) est le plus
|
||||||
|
simple ; le Setup ajoute un raccourci menu Démarrer mais demande un « installer ».
|
||||||
|
|
||||||
|
— Claude
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
---
|
||||||
|
from: claude (Agent A)
|
||||||
|
to: dom
|
||||||
|
date: 2026-06-05T19:30:00+02:00
|
||||||
|
topic: planA-gui-v6-architecture
|
||||||
|
status: open
|
||||||
|
priority: high
|
||||||
|
nature: PLANIFICATION (lecture seule — aucun code modifié, aucun commit)
|
||||||
|
references:
|
||||||
|
- plan: docs/coordination/inbox/for-dom/2026-06-05_claude_plan-v11-5-parallele.md (57aa0f0)
|
||||||
|
- mockup: docs/ui_mockup_v6.html (validé 2026-05-06)
|
||||||
|
- wip: backup/windows-wip-2026-06-05 (b8c9c41)
|
||||||
|
- base_wip: 0124457
|
||||||
|
gardefou: "98 tests unit doivent rester verts — le moteur ne bouge pas"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Plan A — Architecture GUI v6
|
||||||
|
|
||||||
|
Sous-plan détaillé de la transposition GUI v6. **Document de conception
|
||||||
|
uniquement.** Aucun fichier de code n'a été touché.
|
||||||
|
|
||||||
|
## 0. Constat majeur (corrige une hypothèse du plan v11.5)
|
||||||
|
|
||||||
|
Le plan v11.5 décrit le WIP `backup/windows-wip-2026-06-05` comme « +1250 lignes
|
||||||
|
de GUI v6 ». **Vérification faite, ce n'est pas une GUI v6 :**
|
||||||
|
|
||||||
|
- Le diff `0124457..b8c9c41` (+1148/-102) ajoute **profils métier, masques PDF
|
||||||
|
réutilisables, paramètres avancés** — features qui sont **déjà toutes dans le
|
||||||
|
`Pseudonymisation_Gui_V5.py` actuel** (v5.5).
|
||||||
|
- Le WIP est en **tkinter pur** : aucune trace de `customtkinter`/`ctk`/`CTk`.
|
||||||
|
- Le fichier de travail actuel est en fait **en avance** sur le WIP : `git diff
|
||||||
|
backup/windows-wip-2026-06-05 -- Pseudonymisation_Gui_V5.py` = seulement
|
||||||
|
24 insertions / 5 suppressions, et ce delta = les fixes D-11 (VLM masqué hors
|
||||||
|
admin), D-13 (tag « MODE ADMIN » dans le titre) et RGPD (`CHCB`→`CHUXX`,
|
||||||
|
`chcb_strict`→`chuxx_strict`) que le WIP **n'a pas encore**.
|
||||||
|
|
||||||
|
**Conséquences pour l'Agent A :**
|
||||||
|
1. Le WIP n'est **pas** une base de départ v6 — c'est l'ancêtre de la v5.5 actuelle.
|
||||||
|
La « matière première » réelle de la GUI v6 = le **mockup HTML v6** + la **v5.5
|
||||||
|
actuelle** (logique métier déjà écrite et fonctionnelle).
|
||||||
|
2. La GUI v6 = **réécriture de la couche présentation** (tkinter → customtkinter,
|
||||||
|
2 onglets → 3 onglets + sous-onglets) **en réutilisant telle quelle toute la
|
||||||
|
logique métier** (worker, profils, masques, params, contrat moteur).
|
||||||
|
3. La sauvegarde Gitea (section 0 du plan v11.5) est **déjà faite** : la branche
|
||||||
|
existe sur `remotes/gitea/backup/windows-wip-2026-06-05`. Risque « disque
|
||||||
|
unique » levé. ⚠️ reste à vérifier : que la v5.5 *actuelle* (en avance sur le
|
||||||
|
WIP) soit elle aussi sauvegardée hors disque avant de démarrer le codage v6.
|
||||||
|
|
||||||
|
`customtkinter` **n'est ni installé dans `.venv` ni listé dans les requirements**
|
||||||
|
→ à ajouter comme dépendance v11.5 (impact PyInstaller à anticiper avec Agent D).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Inventaire de l'existant
|
||||||
|
|
||||||
|
### 1.1 GUI v5.5 (`Pseudonymisation_Gui_V5.py`, 2894 lignes)
|
||||||
|
|
||||||
|
**Stack :** tkinter + ttk, thème `sv_ttk` optionnel (fallback `clam`), PIL pour
|
||||||
|
logo/icônes (dégradation si absent). Palette magenta/pêche dérivée du logo
|
||||||
|
(`CLR_PRIMARY=#E91E63`, etc.). Onglets *custom* faits main (pas `ttk.Notebook`).
|
||||||
|
|
||||||
|
**Structure actuelle — 3 onglets plats :**
|
||||||
|
|
||||||
|
| Onglet | Contenu |
|
||||||
|
|---|---|
|
||||||
|
| **Anonymisation** | Étape 1 (choisir dossier OU fichier) → Étape 2 (info formats : raster PDF + .txt) → checkbox VLM (si admin) → bouton Lancer/Arrêter → progress → résultats (3 cartes stats + badge fuites + perf + ouvrir dossier + journal repliable) |
|
||||||
|
| **Paramètres** | Whitelist / Blacklist / Stop-words (3 listes éditables) ; masques PDF réutilisables (ouvrir éditeur, combo modèle, dossier modèles) ; export/import JSON ; sauvegarder |
|
||||||
|
| **Profils** | Profil actif (combo + actualiser), description éditable, flags (masque obligatoire, désactiver VLM), masque mémorisé, actions (nouveau/enregistrer/renommer/défaut/supprimer), panneau résumé |
|
||||||
|
|
||||||
|
**Briques techniques déjà en place (à conserver intégralement) :**
|
||||||
|
- `App` (classe monolithique), `UiMessage`/`MsgType` (file worker→UI), `ToolTip`.
|
||||||
|
- Worker threadé (`_run` → `threading.Thread(_worker)`), pompe `_pump_logs`
|
||||||
|
(`root.after(60)`).
|
||||||
|
- Détection police/dark-mode, résolution assets/config compatible PyInstaller
|
||||||
|
(`_asset`, `_app_dir`, `_exe_dir`, `_resolve_config`, `_resolve_profiles_config`).
|
||||||
|
- 4 managers NER chargés en interne (ONNX, EDS-Pseudo, CamemBERT, VLM optionnel).
|
||||||
|
- Mode admin (`admin_mode.is_admin`) : masque le VLM + annote le titre.
|
||||||
|
|
||||||
|
### 1.2 Mockup v6 (`docs/ui_mockup_v6.html`, 898 lignes) — cible UX validée
|
||||||
|
|
||||||
|
**3 onglets principaux :**
|
||||||
|
1. **📄 Utilisation** — dropzone glisser-déposer + liste fichiers, bouton Go,
|
||||||
|
barre progression « Fichier 1/3 », 4 cartes résultats (Documents, PII masqués,
|
||||||
|
Durée, Qualité), bandeau « Aucune fuite détectée », journal.
|
||||||
|
2. **⚙️ Configuration** — **4 sous-onglets** :
|
||||||
|
- **⚙️ Réglages** : catégories PII activables (Noms, Dates naissance,
|
||||||
|
Établissements, Adresses/CP, N° sécu, Tél/email, N° mutuelle) + choix moteur
|
||||||
|
(CamemBERT-bio RAPIDE / EDS-Pseudo PRÉCIS / GLiNER OPTIONNEL).
|
||||||
|
- **🎭 Masquage** : couleur rectangles, libellés placeholders par type
|
||||||
|
(NOM, Date naissance, Établissement…), marges/coins arrondis, **éditeur de
|
||||||
|
masques PDF intégré** (canvas, zoom, DPI, compteur masques, template).
|
||||||
|
- **🔄 Partage** : export/import config (whitelist/blacklist).
|
||||||
|
- **🛡️ Règles** : table de règles personnalisées (Label, Type, Cible→Résultat,
|
||||||
|
Statut) + simulateur (texte test → sortie).
|
||||||
|
3. **ℹ️ À propos** — version, thème, build.
|
||||||
|
|
||||||
|
**Thèmes :** sélecteur (cf. roadmap mémoire : 4 thèmes).
|
||||||
|
|
||||||
|
### 1.3 WIP backup (`b8c9c41`)
|
||||||
|
Ancêtre de la v5.5 (cf. §0). Sert de **référence de lecture** pour les libellés
|
||||||
|
français et l'organisation des écrans Profils/Masques, **pas de base à merger**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Architecture cible GUI v6 (customtkinter)
|
||||||
|
|
||||||
|
### 2.1 Principe directeur
|
||||||
|
**Séparer présentation et logique.** La v5.5 mélange les deux dans une classe
|
||||||
|
`App` de 2894 lignes. La v6 extrait la logique métier (déjà testée, déjà
|
||||||
|
fonctionnelle) dans un *contrôleur* réutilisable, et réécrit uniquement la
|
||||||
|
couche vue en customtkinter selon le mockup.
|
||||||
|
|
||||||
|
### 2.2 Arborescence proposée
|
||||||
|
|
||||||
|
```
|
||||||
|
Pseudonymisation_Gui_V6.py # point d'entrée : main(), bootstrap ctk, App
|
||||||
|
gui_v6/
|
||||||
|
├── __init__.py
|
||||||
|
├── app.py # AppV6(ctk.CTk) : shell, header, nav 3 onglets
|
||||||
|
├── theme.py # palette + 4 thèmes ctk + tokens (couleurs/polices)
|
||||||
|
├── widgets/ # composants réutilisables
|
||||||
|
│ ├── dropzone.py # zone glisser-déposer + liste fichiers
|
||||||
|
│ ├── stat_card.py # carte statistique résultat
|
||||||
|
│ ├── phrase_list.py # liste éditable (whitelist/blacklist/stopwords)
|
||||||
|
│ ├── tooltip.py # ToolTip (porté depuis v5)
|
||||||
|
│ └── tabview.py # onglets/sous-onglets stylés
|
||||||
|
├── tabs/
|
||||||
|
│ ├── tab_use.py # onglet Utilisation
|
||||||
|
│ ├── tab_config.py # onglet Configuration (host des 4 sous-onglets)
|
||||||
|
│ ├── config_reglages.py # sous-onglet Réglages (PII + moteur)
|
||||||
|
│ ├── config_masquage.py # sous-onglet Masquage + éditeur masques intégré
|
||||||
|
│ ├── config_partage.py # sous-onglet Partage (export/import)
|
||||||
|
│ ├── config_regles.py # sous-onglet Règles + simulateur [zone B]
|
||||||
|
│ └── tab_about.py # onglet À propos + état licence [zone C]
|
||||||
|
├── controller.py # AnonymController : SEULE porte vers le moteur
|
||||||
|
├── worker.py # worker threadé + file UiMessage (porté de v5)
|
||||||
|
└── assets_v6/ # logo, icônes (réutilise assets/ existant)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note de packaging :** `gui_v6/` est un package neuf (frontière propre Agent A,
|
||||||
|
cf. plan v11.5 §3). Aucun fichier du périmètre bêta n'est modifié. Ajouter
|
||||||
|
`gui_v6/` et `customtkinter` au `.spec` PyInstaller = tâche post-GO bêta (Agent D).
|
||||||
|
|
||||||
|
### 2.3 Mapping mockup → modules
|
||||||
|
|
||||||
|
| Onglet mockup | Module v6 | Réutilise (v5.5) |
|
||||||
|
|---|---|---|
|
||||||
|
| Utilisation | `tabs/tab_use.py` | `_run`/`_worker`, stats, badge fuites, journal |
|
||||||
|
| Config → Réglages | `tabs/config_reglages.py` | sélection moteur NER, seuils (nouveau : cases PII) |
|
||||||
|
| Config → Masquage | `tabs/config_masquage.py` | combo masques + `pdf_mask_designer` |
|
||||||
|
| Config → Partage | `tabs/config_partage.py` | `_export_params`/`_import_params` |
|
||||||
|
| Config → Règles | `tabs/config_regles.py` | nouveau (zone B) |
|
||||||
|
| À propos | `tabs/tab_about.py` | `_version_long`, build_info (+ licence zone C) |
|
||||||
|
| (Profils v5) | intégré dans Config ou onglet dédié | tout l'appareil `profile_defaults` |
|
||||||
|
|
||||||
|
**Décision à trancher avec Dom :** le mockup v6 n'a **pas** d'onglet « Profils »
|
||||||
|
distinct (v5 en a un). Deux options : (a) garder un 4ᵉ onglet principal
|
||||||
|
« Profils », ou (b) intégrer la sélection de profil en bandeau dans Utilisation +
|
||||||
|
gestion dans Config. Recommandation : **option (b)** pour coller au mockup validé,
|
||||||
|
avec un sélecteur de profil en haut de l'onglet Utilisation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Liste des écrans / workflows
|
||||||
|
|
||||||
|
**Workflow principal (Utilisation) :**
|
||||||
|
1. Glisser-déposer OU parcourir (dossier/fichier) → liste fichiers.
|
||||||
|
2. (option) choisir profil métier + masque manuel.
|
||||||
|
3. Lancer → progress par fichier → cartes résultats + badge fuites → ouvrir dossier.
|
||||||
|
4. Arrêter en cours possible ; journal détaillé repliable.
|
||||||
|
|
||||||
|
**Workflows Configuration :**
|
||||||
|
- Réglages : activer/désactiver catégories PII, choisir moteur NER.
|
||||||
|
- Masquage : couleur/placeholders/marges + dessiner et enregistrer un masque PDF.
|
||||||
|
- Partage : exporter config (JSON pour email) / importer config reçue.
|
||||||
|
- Règles : créer une règle perso (cible→résultat), tester via simulateur.
|
||||||
|
|
||||||
|
**Workflows transverses :** profils (CRUD + défaut), thème (4 thèmes),
|
||||||
|
état licence (bandeau).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Contrat minimal avec le moteur (GARDE-FOU — le moteur ne bouge pas)
|
||||||
|
|
||||||
|
La GUI v6 consomme **exactement les mêmes API que la v5.5**. Aucune signature
|
||||||
|
moteur ne change ⇒ les 98 tests unit restent verts. Tout passe par
|
||||||
|
`gui_v6/controller.py` (point d'entrée unique vers le backend).
|
||||||
|
|
||||||
|
### 4.1 Fonction moteur centrale (à appeler à l'identique)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# anonymizer_core_refactored_onnx.py
|
||||||
|
process_document(doc_path, out_dir, **kwargs) -> Dict[str, str] # multi-formats
|
||||||
|
process_pdf(pdf_path, out_dir, ...) -> Dict[str, str] # fallback PDF
|
||||||
|
```
|
||||||
|
kwargs effectivement passés par le worker v5 (à reproduire tels quels) :
|
||||||
|
`make_vector_redaction=False`, `also_make_raster_burn=True`, `config_path`,
|
||||||
|
`use_hf`, `ner_manager`, `ner_thresholds`, `ogc_label`, `vlm_manager`,
|
||||||
|
`camembert_manager`. Sélection via `getattr(core, 'process_document', None) or
|
||||||
|
core.process_pdf` + clé `doc_path`/`pdf_path`. **Retour = dict chemins de sortie**
|
||||||
|
(clés `audit`, etc.) — la v6 lit ces clés à l'identique (comptage audit, badge fuites).
|
||||||
|
|
||||||
|
### 4.2 Managers NER (instanciés et chargés comme en v5)
|
||||||
|
- `ner_manager_onnx.NerModelManager(cache_dir)` + `NerThresholds` — `.is_loaded()`,
|
||||||
|
`.load(model_id)`, `.models_catalog()`.
|
||||||
|
- `eds_pseudo_manager.EdsPseudoManager(cache_dir)` — idem.
|
||||||
|
- `camembert_ner_manager.CamembertNerManager()` — `.is_loaded()`, `.load()`.
|
||||||
|
- `vlm_manager.VlmManager` / `VlmConfig` — **masqué hors admin** (D-11),
|
||||||
|
`.is_loaded()`.
|
||||||
|
|
||||||
|
### 4.3 Modules support (réutilisés sans modification)
|
||||||
|
- `config_defaults` : `load_effective_dictionaries_dict`, `load_effective_param_lists`,
|
||||||
|
`deep_merge_dict`, `read_*_text`, `ensure_runtime_dictionaries_config`.
|
||||||
|
- `gui_batch_paths` : `list_supported_documents`, `build_batch_output_dir`,
|
||||||
|
`iter_pseudonymized_texts`.
|
||||||
|
- `manual_masking` : `ensure_mask_templates_dir`, `list_mask_templates`,
|
||||||
|
`mask_template_label`, `resolve_manual_mask_pdf`, `append_jsonl_file`.
|
||||||
|
- `profile_defaults` : `list_effective_profiles`, `save_runtime_profile`,
|
||||||
|
`delete_runtime_profile`, `set_runtime_default_profile`, `get_default_profile_key`,
|
||||||
|
`ensure_runtime_profiles_config`.
|
||||||
|
- `pdf_mask_designer` : `Template`, `load_template_yaml`, `apply_template_vector`,
|
||||||
|
`MaskDesignerApp` (intégrer dans le sous-onglet Masquage plutôt que Toplevel).
|
||||||
|
- `format_converter.SUPPORTED_EXTENSIONS`.
|
||||||
|
- `admin_mode.is_admin` / `admin_required`.
|
||||||
|
- `build_info` (BUILD_DATE/COMMIT/BRANCH).
|
||||||
|
|
||||||
|
### 4.4 Construction de la config par profil (logique worker à porter telle quelle)
|
||||||
|
Le worker v5 fabrique un **YAML temporaire** fusionnant config effective +
|
||||||
|
`param_lists` du profil + overlay, puis le passe en `config_path`. Cette mécanique
|
||||||
|
(`deep_merge_dict` + `tempfile.mkstemp` à côté de la config) **est reportée à
|
||||||
|
l'identique** dans `gui_v6/worker.py`. Le moteur reçoit donc le même intrant
|
||||||
|
qu'aujourd'hui → sortie inchangée → audit qualité ≥ baseline.
|
||||||
|
|
||||||
|
**Règle d'or :** `controller.py`/`worker.py` ne contiennent **aucune** logique de
|
||||||
|
détection. Ils orchestrent. Toute tentation de « pré-traiter » le texte côté GUI
|
||||||
|
= violation du garde-fou.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Stratégie de migration progressive (v5 → v6 sans casser)
|
||||||
|
|
||||||
|
1. **Cohabitation.** v6 = fichier neuf `Pseudonymisation_Gui_V6.py` + package
|
||||||
|
`gui_v6/`. La v5.5 reste l'entrée par défaut tant que la v6 n'a pas passé le
|
||||||
|
smoke test et l'audit qualité. Bascule par défaut = dernière étape (Agent D).
|
||||||
|
2. **Extraction d'abord, vue ensuite.** Étape 1 : extraire worker + contrôleur
|
||||||
|
depuis la v5.5 **sans changer de toolkit** (refactor pur, testable). Étape 2 :
|
||||||
|
réécrire la vue en customtkinter par-dessus ce contrôleur. Ça découple le risque
|
||||||
|
« moteur » du risque « UI ».
|
||||||
|
3. **Parité fonctionnelle par onglet.** Migrer Utilisation → Configuration →
|
||||||
|
Profils dans cet ordre ; à chaque onglet, vérifier que le workflow produit les
|
||||||
|
**mêmes sorties** que la v5 sur un même lot (diff des dossiers `anonymise/`).
|
||||||
|
4. **Tests conservés.** `gui_batch_paths` / `manual_masking` ont déjà leurs tests :
|
||||||
|
ne pas y toucher. Ajouter un **smoke test de lancement** v6 + un test de
|
||||||
|
**non-régression du contrat** (mocked managers, vérifier que le worker appelle
|
||||||
|
`process_document` avec exactement les kwargs attendus).
|
||||||
|
5. **Garde-fou n°1 permanent.** `pytest tests/unit` (98) doit rester vert à chaque
|
||||||
|
commit v6. Si un test moteur casse ⇒ la v6 a franchi sa frontière, rollback.
|
||||||
|
6. **Rétro-port RGPD/admin.** La v6 doit naître au niveau de la v5.5 **actuelle**
|
||||||
|
(CHUXX, admin tag, VLM masqué), pas du WIP `b8c9c41` qui est en retard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Zones de contact
|
||||||
|
|
||||||
|
### 6.1 Avec Agent B (D-13 — Paramètres avancés / Profils techniques)
|
||||||
|
- **Fichiers partagés :** sous-onglets « avancés » de Config (`config_reglages.py`,
|
||||||
|
`config_regles.py`) + onglet/bandeau Profils.
|
||||||
|
- **Contrat attendu de B (avant que A code ces écrans) :**
|
||||||
|
- liste des réglages **protégés admin** (cachés/désactivés en non-admin) ;
|
||||||
|
- API `admin_mode.admin_required(feature)` pour verrouiller une action ;
|
||||||
|
- règle de sauvegarde : config sensible **bloquée** en non-admin.
|
||||||
|
- **A fournit :** des conteneurs/onglets prêts où B injecte ses contrôles +
|
||||||
|
un helper `is_admin()` déjà câblé dans le shell (titre annoté, sections
|
||||||
|
masquées). A réserve le sous-onglet « Règles » comme zone B.
|
||||||
|
- **À écrire :** contrat A↔B avant tout code (plan v11.5 §3).
|
||||||
|
|
||||||
|
### 6.2 Avec Agent C (Licence — affichage état)
|
||||||
|
- **Emplacement UI réservé par A :** bandeau d'état en haut du shell (sous le
|
||||||
|
header) + bloc dédié dans l'onglet **À propos** (statut, expiration, grace).
|
||||||
|
- **API attendue de C (`license.py`, à créer) :** une fonction de statut du type
|
||||||
|
`get_license_status() -> {valid, expires_at, grace_days, machine_id, message}`
|
||||||
|
que A appelle au démarrage et affiche (vert/orange/rouge). A **n'implémente
|
||||||
|
aucune crypto** ; A consomme le statut.
|
||||||
|
- **Dégradation :** si `license.py` absent (dev), le bandeau s'efface
|
||||||
|
silencieusement (même pattern que `admin_mode`/`vlm_manager` en try/except).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Risques spécifiques GUI v6 + mitigations
|
||||||
|
|
||||||
|
| Risque | Mitigation |
|
||||||
|
|---|---|
|
||||||
|
| customtkinter absent du venv/spec | Ajouter dépendance + tester build EXE tôt avec Agent D |
|
||||||
|
| Éditeur de masques (`MaskDesignerApp`) conçu pour Toplevel tk | L'intégrer en frame dans le sous-onglet Masquage, ou le garder en fenêtre détachée v1 |
|
||||||
|
| Glisser-déposer natif (mockup) absent de tkinter pur | `tkinterdnd2` ou fallback « Parcourir » ; à valider avec Dom |
|
||||||
|
| Régression silencieuse moteur via worker | Test contrat (kwargs `process_document`) + 98 tests verts |
|
||||||
|
| v6 part du WIP en retard (CHCB/admin) | Naître de la v5.5 actuelle (§5.6) |
|
||||||
|
| Dérive de portée (refonte logique) | controller/worker = orchestration pure, zéro détection |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Résumé (5-8 lignes)
|
||||||
|
|
||||||
|
Le « WIP +1250 lignes » n'est **pas** une GUI v6 : c'est l'ancêtre tkinter de la
|
||||||
|
v5.5 actuelle (profils/masques/params déjà livrés), et la v5.5 est même **en
|
||||||
|
avance** dessus (fixes CHUXX/admin/VLM). La vraie matière première v6 = le
|
||||||
|
**mockup HTML validé** + la **logique métier v5.5** à réutiliser telle quelle.
|
||||||
|
Architecture cible : `Pseudonymisation_Gui_V6.py` + package `gui_v6/`
|
||||||
|
(customtkinter, **à ajouter au venv/spec**), 3 onglets (Utilisation / Configuration
|
||||||
|
à 4 sous-onglets / À propos), éditeur de masques intégré, 4 thèmes. **Garde-fou
|
||||||
|
n°1 :** tout passe par `gui_v6/controller.py`+`worker.py` qui appellent
|
||||||
|
`process_document(...)` avec les **mêmes kwargs** qu'aujourd'hui → le moteur ne
|
||||||
|
bouge pas, les 98 tests unit restent verts. Migration progressive : extraire
|
||||||
|
contrôleur d'abord, réécrire la vue ensuite, cohabitation v5/v6, bascule par
|
||||||
|
défaut en dernier (Agent D). Zones de contact : **B** sur les sous-onglets
|
||||||
|
Réglages/Règles + Profils (contrat admin écrit avant code), **C** sur le bandeau
|
||||||
|
d'état + bloc À propos (A consomme `get_license_status()`, A n'implémente aucune
|
||||||
|
crypto).
|
||||||
|
|
||||||
|
— Claude (Agent A)
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
---
|
||||||
|
from: claude (Agent B)
|
||||||
|
to: dom
|
||||||
|
date: 2026-06-05T19:30:00+02:00
|
||||||
|
topic: planB-d13-complet
|
||||||
|
status: open
|
||||||
|
priority: high
|
||||||
|
scope: PLANIFICATION uniquement — lecture seule, aucun code modifié
|
||||||
|
references:
|
||||||
|
- plan: docs/coordination/inbox/for-dom/2026-06-05_claude_plan-v11-5-parallele.md (commit 57aa0f0)
|
||||||
|
- decision: docs/coordination/decisions/2026-06-02_dom_d13-partial-scope.md
|
||||||
|
- module: admin_mode.py
|
||||||
|
- gui: Pseudonymisation_Gui_V5.py (2893 lignes, v5.4)
|
||||||
|
---
|
||||||
|
|
||||||
|
# Plan B — D-13 complet : protection des réglages avancés en mode non-admin (GUI v6)
|
||||||
|
|
||||||
|
> **Périmètre.** Sous-plan du chantier v11.5 (cap D-17). Définit les **règles**
|
||||||
|
> admin / non-admin à appliquer dans la GUI v6 (customtkinter). Ne contient aucun
|
||||||
|
> code ; l'implémentation attend le GO bêta (D-16) et se fait dans `gui_v6/` +
|
||||||
|
> extension `admin_mode.py`. Les **écrans** des sections « avancé » sont co-conçus
|
||||||
|
> avec l'Agent A (voir § Zone de contact A↔B).
|
||||||
|
|
||||||
|
## 0. Rappel du modèle de menace D-13
|
||||||
|
|
||||||
|
Le mode admin n'est **pas** un contrôle d'accès cryptographique : c'est un
|
||||||
|
« verrou anti-distrait » (cf. docstring `admin_mode.py`). Activation par
|
||||||
|
`ANON_ADMIN=1` ou fichier `.admin`. Deux objectifs distincts, à ne pas confondre :
|
||||||
|
|
||||||
|
1. **Anti-leak RGPD** (critique) — empêcher l'envoi de données hors poste.
|
||||||
|
Déjà couvert par D-11 : VLM/Ollama **caché** en non-admin, et de toute façon
|
||||||
|
`VlmManager=None` quand le module est neutralisé.
|
||||||
|
2. **Anti-dégradation qualité** (important, périmètre v11.5) — empêcher le
|
||||||
|
bêta-testeur / utilisateur final de **casser la détection** en éditant des
|
||||||
|
stopwords, profils techniques, regex, ou d'**écraser des fichiers de config
|
||||||
|
de référence**. C'est l'objet de ce plan.
|
||||||
|
|
||||||
|
Conséquence : pour les réglages qui ne provoquent **pas** de fuite externe mais
|
||||||
|
peuvent **dégrader le masquage**, la bonne politique par défaut est
|
||||||
|
**griser/désactiver (visible mais verrouillé)** plutôt que **cacher**, pour rester
|
||||||
|
pédagogique. Exception : ce qui touche au leak externe se **cache**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Inventaire exhaustif des réglages exposés
|
||||||
|
|
||||||
|
Source : balayage de `Pseudonymisation_Gui_V5.py`, `config/`, `config_defaults.py`,
|
||||||
|
`profile_defaults.py`.
|
||||||
|
|
||||||
|
### 1.A — Réglages UI (widgets actuels v5)
|
||||||
|
|
||||||
|
| # | Réglage | Widget v5 | Variable / méthode | Écrit dans |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| R1 | **Analyse visuelle VLM (Ollama)** | `Checkbutton` (l.769) | `self.use_vlm` / `_on_vlm_toggle` | aucun (runtime) |
|
||||||
|
| R2 | **Profil : « Désactiver le VLM »** | `Checkbutton` (l.1088) | `profile_force_disable_vlm_var` | `profiles.yml` |
|
||||||
|
| R3 | **Whitelist — phrases à NE PAS anonymiser** | `Listbox` + ajout/suppr (l.919) | `_wl_listbox` | `dictionnaires.yml` → `whitelist_phrases` |
|
||||||
|
| R4 | **Blacklist — mots à TOUJOURS masquer** | `Listbox` (l.928) | `_bl_listbox` | `dictionnaires.yml` → `blacklist.force_mask_terms` |
|
||||||
|
| R5 | **Stop-words additionnels** (ne jamais traiter comme nom) | `Listbox` (l.939) | `_sw_listbox` | `dictionnaires.yml` → `additional_stopwords` |
|
||||||
|
| R6 | **Profil actif** (sélection) | `Combobox` (l.1032) | `processing_profile_label_var` | lecture seule |
|
||||||
|
| R7 | **Profil : description** | `Entry` (l.1064) | `profile_description_var` | `profiles.yml` |
|
||||||
|
| R8 | **Profil : « Masque manuel obligatoire »** | `Checkbutton` (l.1077) | `profile_require_manual_mask_var` | `profiles.yml` |
|
||||||
|
| R9 | **Profil : masque PDF mémorisé** | `Combobox` (l.1109) | `manual_mask_template_var` | `profiles.yml` |
|
||||||
|
| R10 | **Créer / Renommer / Supprimer / Définir par défaut un profil** | Boutons (l.2040-2147) | `_create/_rename/_delete/_set_default_…profile` | `profiles.yml` |
|
||||||
|
| R11 | **Sauvegarder le profil courant** | Bouton (l.2147) | `_save_selected_processing_profile` | `profiles.yml` |
|
||||||
|
| R12 | **Masque manuel : template actif** (anonymisation) | `Combobox` (l.881) | `manual_mask_template_var` | runtime |
|
||||||
|
| R13 | **Éditeur de masques PDF** (designer) | Bouton (l.2306) | `_open_manual_mask_designer` | `config/mask_templates/` |
|
||||||
|
| R14 | **Sauvegarder les paramètres** (WL/BL/SW → YAML) | Bouton (l.2629) | `_save_params` → `_save_param_listboxes` | **`dictionnaires.yml` (écriture)** |
|
||||||
|
| R15 | **Exporter les paramètres** (→ JSON) | Bouton (l.2539) | `_export_params` | JSON sur disque (Bureau) |
|
||||||
|
| R16 | **Importer des paramètres** (JSON → listes) | Bouton (l.2596) | `_import_params` | listes en mémoire |
|
||||||
|
| R17 | **Dossier / fichier source** | sélecteur | `dir_var` / `_single_file` | runtime |
|
||||||
|
|
||||||
|
### 1.B — Réglages présents dans le **schéma de config** mais PAS exposés en UI v5
|
||||||
|
|
||||||
|
Importants à connaître car la GUI v6 pourrait vouloir les exposer (profils
|
||||||
|
techniques). Aujourd'hui chargés silencieusement (en-tête v5 : « Pas d'onglet
|
||||||
|
Avancé (NER + YAML chargés silencieusement) »).
|
||||||
|
|
||||||
|
| # | Réglage | Fichier / clé | Statut v5 | Sensibilité |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| S1 | **`regex_overrides`** (patterns + placeholders custom) | `dictionnaires.yml` → `regex_overrides[]` | non exposé UI | **technique sensible** (une mauvaise regex casse la détection ou plante) |
|
||||||
|
| S2 | **`blacklist.force_mask_regex`** | `dictionnaires.yml` | non exposé UI | technique sensible |
|
||||||
|
| S3 | **`whitelist.org_gpe_keep` / `sections_titres` / `noms_maj_excepts`** | `dictionnaires.yml` → `whitelist.*` | non exposé UI | technique sensible (peut désactiver le masquage d'établissements) |
|
||||||
|
| S4 | **`kv_labels_preserve`** | `dictionnaires.yml` | non exposé UI | technique sensible |
|
||||||
|
| S5 | **`flags.regex_engine` / `case_insensitive` / `unicode_word_boundaries`** | `dictionnaires.yml` → `flags` | non exposé UI | technique sensible |
|
||||||
|
| S6 | **`additional_villes_blacklist` / `additional_dpi_labels` / `additional_companion_blacklist`** | `dictionnaires.yml` | non exposé UI | modérée (qualité) |
|
||||||
|
| S7 | **`dictionaries_overlay`** par profil (surcharge YAML embarquée) | `profiles.yml` → `dictionaries_overlay` | partiellement (via BL profil) | **technique sensible** |
|
||||||
|
| S8 | **Choix du moteur NER** (GLiNER / CamemBERT-bio / EDS-Pseudo / ONNX) | aucun fichier UI ; chargé via `_auto_load_ner()` (l.473), managers l.437-439 | **non exposé** (silencieux) | **technique sensible** (désactiver un moteur dégrade le recall F1=0.963) |
|
||||||
|
| S9 | **Seuils NER** (`NerThresholds`) | `ner_manager_onnx.py` | non exposé UI | technique sensible |
|
||||||
|
| S10 | **Chemins config** (`cfg_path`, `profiles_path`, `MODELS_DIR`) | `DEFAULT_CFG`, `DEFAULT_PROFILES_CFG` | non exposé UI (pas de file picker) | sensible (réécriture d'un autre fichier) |
|
||||||
|
|
||||||
|
> Note : le **choix du moteur NER** (reporté à v11.5 selon D-13) n'a **aucune UI
|
||||||
|
> aujourd'hui**. L'exposer en v6 est une **création** d'écran, donc à protéger
|
||||||
|
> dès l'origine. Recommandation forte : **réservé admin**, et même en admin,
|
||||||
|
> exposer en lecture/diagnostic plutôt qu'en désactivation libre, pour ne pas
|
||||||
|
> permettre de couper un moteur et faire chuter le recall sans le vouloir.
|
||||||
|
|
||||||
|
### 1.C — Fichiers de config sensibles (cibles d'écriture)
|
||||||
|
|
||||||
|
| Fichier | Rôle | Écriture en v5 par | Politique non-admin |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `config/dictionnaires.yml` | surcharge locale active (WL/BL/SW/regex) | R14 `_save_param_listboxes` | **bloquer l'écriture** |
|
||||||
|
| `config/dictionnaires.default.yml` | **source de vérité** | jamais (ne doit jamais l'être) | **bloquer (admin compris)** |
|
||||||
|
| `config/profiles.yml` | profils locaux | R10/R11 | **bloquer écriture** (lecture/sélection OK) |
|
||||||
|
| `config/profiles.default.yml` | source de vérité profils | jamais | **bloquer (admin compris)** |
|
||||||
|
| `config/admin_rules.yml` | règles d'admin candidates | (gouvernance) | **bloquer** |
|
||||||
|
| `config/mask_templates/`, `config/mask_templates` GUI | masques PDF | R13 designer | autorisé (non sensible PII) |
|
||||||
|
| Export JSON (Bureau) | échange par email | R15 | **autorisé** (sortie, pas d'écrasement config) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Matrice admin / non-admin
|
||||||
|
|
||||||
|
Légende : **V** = visible, **É** = éditable, **S** = sauvegardable (peut écrire un fichier).
|
||||||
|
`—` = non applicable. `(cacher)` = absent de l'UI. `(grisé)` = visible mais désactivé.
|
||||||
|
|
||||||
|
| # | Réglage | non-admin V | non-admin É | non-admin S | admin V | admin É | admin S | Mode UI non-admin |
|
||||||
|
|---|---|:--:|:--:|:--:|:--:|:--:|:--:|---|
|
||||||
|
| R1 | VLM Ollama (case) | ❌ | ❌ | — | ✅ | ✅ | — | **cacher** (leak RGPD) |
|
||||||
|
| R2 | Profil « Désactiver VLM » | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** (lié VLM) |
|
||||||
|
| R3 | Whitelist phrases | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** (lecture) |
|
||||||
|
| R4 | Blacklist force-mask | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** (lecture) |
|
||||||
|
| R5 | Stop-words additionnels | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** (lecture) |
|
||||||
|
| R6 | Profil actif (sélection) | ✅ | ✅ | — | ✅ | ✅ | — | **actif** (choisir un profil pré-validé est sûr) |
|
||||||
|
| R7 | Profil : description | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** |
|
||||||
|
| R8 | Profil : masque manuel obligatoire | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** |
|
||||||
|
| R9 | Profil : masque PDF mémorisé | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** |
|
||||||
|
| R10 | Créer/Renommer/Suppr/Défaut profil | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** (écrit profiles.yml) |
|
||||||
|
| R11 | Sauvegarder le profil | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** |
|
||||||
|
| R12 | Masque manuel actif (anonymisation) | ✅ | ✅ | — | ✅ | ✅ | — | **actif** (sécurité, ajoute du masquage) |
|
||||||
|
| R13 | Éditeur masques PDF | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | **actif** (n'augmente jamais le leak) |
|
||||||
|
| R14 | Sauvegarder paramètres → YAML | ❌ | — | ❌ | ✅ | — | ✅ | **cacher** (écrit dictionnaires.yml) |
|
||||||
|
| R15 | Exporter paramètres (JSON) | ✅ | — | ✅ | ✅ | — | ✅ | **actif** (sortie d'échange, pas d'écrasement config) |
|
||||||
|
| R16 | Importer paramètres (JSON) | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | **actif en mémoire**, mais **R14 bloqué** → l'import reste sans effet persistant en non-admin (voir § 3.3) |
|
||||||
|
| R17 | Dossier / fichier source | ✅ | ✅ | — | ✅ | ✅ | — | **actif** (cœur métier) |
|
||||||
|
| S1 | `regex_overrides` (si exposé v6) | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** (profil technique) |
|
||||||
|
| S2 | `force_mask_regex` | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** |
|
||||||
|
| S3 | `whitelist.*` techniques | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** |
|
||||||
|
| S4 | `kv_labels_preserve` | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** |
|
||||||
|
| S5 | `flags.*` (regex_engine…) | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** |
|
||||||
|
| S6 | `additional_villes/dpi/companion` | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** (qualité, lecture) |
|
||||||
|
| S7 | `dictionaries_overlay` profil | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** |
|
||||||
|
| S8 | Choix moteur NER | ✅ (diag) | ❌ | ❌ | ✅ | ✅* | ❌* | **grisé/diagnostic** ; *même admin : lecture conseillée (voir § 3.4) |
|
||||||
|
| S9 | Seuils NER | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** |
|
||||||
|
| S10 | Chemins config (file pickers) | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** (ne pas exposer en v6 hors admin) |
|
||||||
|
|
||||||
|
**Principe de lecture de la matrice** :
|
||||||
|
- **Cacher** = réglage *leak-sensible* (VLM) ou *technique avancé* (regex/profil
|
||||||
|
technique/moteur/seuils/chemins). Inutile et risqué de le montrer au bêta.
|
||||||
|
- **Griser** = réglage *qualité* visible à titre pédagogique (le bêta voit ce que
|
||||||
|
l'admin a configuré) mais non modifiable / non sauvegardable.
|
||||||
|
- **Actif** = réglage qui *ne peut qu'augmenter la sécurité* (ajouter du masquage :
|
||||||
|
masque manuel R12/R13) ou *cœur métier* (R6 sélection de profil validé, R17
|
||||||
|
source), ou *sortie sans écrasement* (R15 export).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Règles UI et règles de sauvegarde
|
||||||
|
|
||||||
|
### 3.1 — Cacher vs désactiver/griser (décision par catégorie)
|
||||||
|
|
||||||
|
| Catégorie | Politique non-admin | Justification |
|
||||||
|
|---|---|---|
|
||||||
|
| **Leak externe** (VLM, force_disable_vlm) | **Cacher** | Ne doit pas exister dans l'UI bêta (D-11). |
|
||||||
|
| **Technique avancé** (regex_overrides, force_mask_regex, whitelist.*, flags, kv_labels, dictionaries_overlay, seuils NER, chemins config) | **Cacher** | Bruit pour le bêta + casse silencieuse de la détection. Regrouper dans un onglet/section « Profils techniques » entièrement masquée hors admin. |
|
||||||
|
| **Qualité éditable** (WL/BL/SW, descriptions, flags profil, villes/dpi/companion) | **Griser (read-only)** | Pédagogique : le bêta voit la config sans pouvoir la dégrader. |
|
||||||
|
| **Gestion de profils** (CRUD, sauvegarde profil) | **Cacher** | Écrit `profiles.yml`. |
|
||||||
|
| **Sécurité additive** (masque manuel actif, designer, sélection profil, source) | **Actif** | Ne réduit jamais le masquage. |
|
||||||
|
| **Échange** (export JSON) | **Actif** ; **import** actif en mémoire mais sans persistance (R14 bloqué) | Sortie pour email, pas d'écrasement de config. |
|
||||||
|
|
||||||
|
### 3.2 — Implémentation UI (customtkinter v6)
|
||||||
|
|
||||||
|
1. **Cacher** : ne pas instancier le widget/onglet quand `not is_admin()`.
|
||||||
|
L'onglet/section « Profils techniques » et la section VLM ne sont **pas
|
||||||
|
créés** hors admin (pas seulement `grid_remove`, pour éviter toute
|
||||||
|
réactivation accidentelle).
|
||||||
|
2. **Griser** : créer le widget puis `configure(state="disabled")`. Pour les
|
||||||
|
`Listbox`/listes éditables : désactiver les boutons +Ajouter / Supprimer et
|
||||||
|
passer la liste en lecture seule ; afficher un bandeau discret
|
||||||
|
« Réglages avancés en lecture seule — mode admin requis pour modifier ».
|
||||||
|
3. **Helper centralisé** (extension `admin_mode.py`) :
|
||||||
|
- `admin_only_visible(widget)` → ne crée/affiche que si admin.
|
||||||
|
- `admin_only_editable(widget)` → `state="normal"` si admin sinon `"disabled"`.
|
||||||
|
- `guard_save(feature) ` → wrappe l'écriture (voir 3.3).
|
||||||
|
Cela centralise la logique au lieu de la disperser dans la GUI (frontière
|
||||||
|
Agent B : `admin_mode.py` + sections « avancé » de `gui_v6/`).
|
||||||
|
4. **Titre fenêtre** : conserver le tag `[⚙ MODE ADMIN]` (déjà en v5, l.383-384).
|
||||||
|
En v6, ajouter une **bannière** persistante en mode admin (rouge/orange).
|
||||||
|
|
||||||
|
### 3.3 — Règles de sauvegarde (blocage de l'écriture des fichiers sensibles)
|
||||||
|
|
||||||
|
Le point dur de D-13 complet : **l'UI masquée ne suffit pas**. Il faut un garde
|
||||||
|
au niveau de l'**écriture** pour qu'aucun chemin (raccourci clavier, import +
|
||||||
|
save, futur bouton) ne puisse écraser un fichier sensible hors admin.
|
||||||
|
|
||||||
|
1. **Garde à la source** : toute méthode qui écrit un fichier de config sensible
|
||||||
|
doit appeler `admin_required(...)` (déjà fourni par `admin_mode.py`) **avant**
|
||||||
|
l'écriture. Cibles : `_save_param_listboxes` (R14 → `dictionnaires.yml`),
|
||||||
|
`_save_selected_processing_profile`, `_create/_rename/_delete/_set_default…`
|
||||||
|
(R10/R11 → `profiles.yml`), et toute future écriture de `regex_overrides`,
|
||||||
|
`dictionaries_overlay`, `flags`, seuils.
|
||||||
|
2. **Liste blanche d'écriture** : définir dans `admin_mode.py` un ensemble
|
||||||
|
`SENSITIVE_CONFIG_FILES = {dictionnaires.yml, dictionnaires.default.yml,
|
||||||
|
profiles.yml, profiles.default.yml, admin_rules.yml, hospital_stopwords.yml,
|
||||||
|
medical_terms_whitelist.yml}` + une fonction `assert_writable(path)` qui lève
|
||||||
|
si `path` est sensible et `not is_admin()`. Appelée par tous les `write_text`
|
||||||
|
de config. Filet de sécurité indépendant de l'UI.
|
||||||
|
3. **`*.default.yml` jamais réécrits** — même en admin. `assert_writable` refuse
|
||||||
|
l'écriture des `*.default.yml` quel que soit le mode (sources de vérité).
|
||||||
|
4. **Import JSON (R16)** : autorisé à charger en mémoire (pas de fuite), mais le
|
||||||
|
bouton **Sauvegarder (R14) étant caché/bloqué en non-admin**, l'import reste
|
||||||
|
sans effet persistant. À documenter dans l'UI : « Import chargé. Sauvegarde
|
||||||
|
réservée au mode admin. » Évite de laisser croire que la config est modifiée.
|
||||||
|
5. **Export JSON (R15)** : autorisé en non-admin — c'est une **sortie** vers le
|
||||||
|
Bureau pour échange par email, pas un écrasement de config. (Cohérent avec le
|
||||||
|
workflow « export → merge → renvoi YAML » des préférences projet.)
|
||||||
|
|
||||||
|
### 3.4 — Cas particulier : choix du moteur NER (S8)
|
||||||
|
|
||||||
|
Reporté à v11.5 par D-13 mais **sans UI existante**. Recommandation :
|
||||||
|
- Hors admin : **non exposé** (ou bandeau diagnostic en lecture seule listant les
|
||||||
|
moteurs chargés : EDS-Pseudo / GLiNER / CamemBERT-bio / ONNX + état).
|
||||||
|
- En admin : exposer en **diagnostic** (voir/recharger) ; **déconseiller** une
|
||||||
|
case « désactiver moteur X » librement, car couper un moteur fait chuter le
|
||||||
|
recall (multi-signal F1=0.963). Si Dom veut le toggle, l'assortir d'un
|
||||||
|
avertissement explicite « peut réduire la détection ». À trancher par Dom.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Tests attendus (matrice admin / non-admin)
|
||||||
|
|
||||||
|
Tests pilotables sans GUI réelle en testant les **helpers** + les **gardes
|
||||||
|
d'écriture** ; tests GUI en smoke (`gui_v6`). Cible : `tests/unit/test_d13_admin_*`.
|
||||||
|
|
||||||
|
### 4.A — `admin_mode` (logique)
|
||||||
|
- `is_admin()` : `ANON_ADMIN ∈ {1,true,yes,on}` → True ; vide/0 → False ;
|
||||||
|
fichier `.admin` présent → True ; `force_refresh` re-évalue le cache.
|
||||||
|
- `admin_required("x")` : lève `RuntimeError` hors admin, ne lève pas en admin.
|
||||||
|
- `assert_writable(path)` (nouveau) :
|
||||||
|
- fichier sensible + non-admin → lève ;
|
||||||
|
- fichier sensible + admin → OK **sauf** `*.default.yml` → lève (toujours) ;
|
||||||
|
- fichier non sensible (export JSON, mask_templates) → OK dans les deux modes.
|
||||||
|
|
||||||
|
### 4.B — Matrice par réglage (paramétrée admin ∈ {False, True})
|
||||||
|
Pour chaque réglage R1–R17 / S1–S10, asserter la cible de la matrice § 2 :
|
||||||
|
|
||||||
|
| Assertion | non-admin attendu | admin attendu |
|
||||||
|
|---|---|---|
|
||||||
|
| widget créé (visible) | selon col. « non-admin V » | « admin V » |
|
||||||
|
| widget `state` | `disabled` si grisé, absent si caché | `normal` |
|
||||||
|
| la sauvegarde écrit le fichier | **non** (lève / no-op) pour R10/R11/R14/S* | **oui** |
|
||||||
|
| `dictionnaires.yml` / `profiles.yml` non modifiés après tentative non-admin | hash fichier inchangé | modifié après save admin |
|
||||||
|
|
||||||
|
### 4.C — Non-régression (garde-fou n°1 du plan maître)
|
||||||
|
- `tests/unit` (98 passed) **restent verts** — D-13 ne touche pas le moteur.
|
||||||
|
- Audit `evaluate_quality.py` ≥ 98.5 ; leak score 100/100 inchangé.
|
||||||
|
- Smoke v6 : lancement non-admin → aucune section technique/VLM présente ;
|
||||||
|
lancement admin (`ANON_ADMIN=1`) → sections présentes + bannière admin.
|
||||||
|
|
||||||
|
### 4.D — Test « anti-contournement »
|
||||||
|
- Simuler import JSON (R16) puis tentative de save (R14) en non-admin →
|
||||||
|
`dictionnaires.yml` **inchangé**.
|
||||||
|
- Vérifier qu'aucun `write_text` sur un fichier sensible n'est atteignable hors
|
||||||
|
`assert_writable` (revue : grep des `write_text` sur `config/` dans `gui_v6/`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Impacts GUI v5 vs GUI v6
|
||||||
|
|
||||||
|
### GUI v5 (`Pseudonymisation_Gui_V5.py`) — **laisser tel quel**
|
||||||
|
- D-13 **partiel** est déjà livré et acté (VLM caché, titre admin). Conforme au
|
||||||
|
gel bêta (D-16) : on ne re-patche pas 2893 lignes tkinter.
|
||||||
|
- **Aucune modification v5** dans ce chantier. (Si un hotfix MVP devenait
|
||||||
|
nécessaire, il resterait hors périmètre v11.5.)
|
||||||
|
|
||||||
|
### GUI v6 (`Pseudonymisation_Gui_V6.py` / `gui_v6/`) — **lieu d'implémentation**
|
||||||
|
- D-13 **complet** s'implémente nativement à la construction de chaque écran v6,
|
||||||
|
via les helpers `admin_mode` (§ 3.2). Pas de rétro-fit : la visibilité/édition
|
||||||
|
est décidée **au moment de créer le widget**.
|
||||||
|
- Frontières (plan maître § 3) : Agent B possède `admin_mode.py` (extension :
|
||||||
|
`assert_writable`, `SENSITIVE_CONFIG_FILES`, helpers UI) et les **règles** des
|
||||||
|
sections « avancé » ; Agent A possède les écrans `gui_v6/`.
|
||||||
|
- Structure cible v6 (proposition) : un onglet **« Profils techniques »** + une
|
||||||
|
section **VLM** entièrement **conditionnés à `is_admin()`** (non instanciés
|
||||||
|
hors admin) ; la section **« Paramètres avancés »** (WL/BL/SW) **toujours
|
||||||
|
visible** mais **read-only** hors admin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Zone de contact Agent A ↔ Agent B (contrat à figer avant code)
|
||||||
|
|
||||||
|
Les écrans « Paramètres avancés » et « Profils techniques » de la GUI v6 sont
|
||||||
|
**co-conçus** : **B fournit les règles, A fournit les écrans**. Contrat proposé :
|
||||||
|
|
||||||
|
**Ce que B (ce plan) fournit à A :**
|
||||||
|
1. La **matrice § 2** (visible/éditable/sauvegardable par réglage et par mode).
|
||||||
|
2. Les **helpers** `admin_mode` (signatures) que A appellera :
|
||||||
|
- `is_admin() -> bool`
|
||||||
|
- `admin_only_visible(parent, build_fn)` — n'appelle `build_fn` que si admin.
|
||||||
|
- `admin_only_editable(widget)` — applique `state`.
|
||||||
|
- `assert_writable(path)` — à appeler avant toute écriture config.
|
||||||
|
3. La **liste des écritures à garder** (R10/R11/R14, futurs S1/S7/S5…).
|
||||||
|
4. La **convention de regroupement** : tout réglage « technique avancé »
|
||||||
|
(S1–S5, S7, S9, S10, R2) dans **un seul** conteneur masquable d'un bloc.
|
||||||
|
|
||||||
|
**Ce que A fournit à B :**
|
||||||
|
1. Les conteneurs/onglets v6 nommés (où s'accrochent les sections « avancé »).
|
||||||
|
2. L'emplacement de la **bannière mode admin** (cohérence avec la bannière
|
||||||
|
licence réservée à l'Agent C).
|
||||||
|
3. Le point d'appel unique des écritures de config (pour y placer
|
||||||
|
`assert_writable`) afin d'éviter des `write_text` dispersés.
|
||||||
|
|
||||||
|
**À trancher par Dom :**
|
||||||
|
- S8 (toggle moteur NER) en admin : **diagnostic seul** (recommandé) ou toggle
|
||||||
|
avec avertissement ?
|
||||||
|
- Import JSON (R16) en non-admin : garder l'import-en-mémoire (proposé) ou le
|
||||||
|
cacher aussi ?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Synthèse des recommandations
|
||||||
|
|
||||||
|
1. **Deux politiques** : *cacher* le leak-sensible (VLM) et le technique avancé
|
||||||
|
(regex/profils techniques/moteur/seuils/chemins) ; *griser* le qualité
|
||||||
|
(WL/BL/SW) ; *laisser actif* l'additif-sécurité (masques) et l'export.
|
||||||
|
2. **Garde d'écriture indépendante de l'UI** (`assert_writable` +
|
||||||
|
`SENSITIVE_CONFIG_FILES`) : filet de sécurité contre tout contournement.
|
||||||
|
`*.default.yml` jamais réécrits, même en admin.
|
||||||
|
3. **GUI v5 inchangée** ; tout dans `gui_v6/` + extension `admin_mode.py`.
|
||||||
|
4. **Tests** : matrice paramétrée admin/non-admin + anti-contournement + 98 tests
|
||||||
|
moteur verts (garde-fou non négociable).
|
||||||
|
5. **Contrat A↔B** figé avant tout code (helpers + matrice + points d'écriture).
|
||||||
|
6. **Attente GO bêta (D-16)** avant tout codage — ce document est de la
|
||||||
|
planification pure.
|
||||||
|
|
||||||
|
— Claude (Agent B)
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
---
|
||||||
|
from: claude (Agent C — chantier v11.5)
|
||||||
|
to: dom
|
||||||
|
date: 2026-06-05T19:30:00+02:00
|
||||||
|
topic: planC-licence-d14
|
||||||
|
status: open
|
||||||
|
priority: high
|
||||||
|
references:
|
||||||
|
- decision: docs/coordination/decisions/2026-06-02_dom_d14-plateforme-licence-architecture.md
|
||||||
|
- plan: docs/coordination/inbox/for-dom/2026-06-05_claude_plan-v11-5-parallele.md
|
||||||
|
scope: CONCEPTION uniquement — aucun code de prod, aucun déploiement
|
||||||
|
---
|
||||||
|
|
||||||
|
# Plan C — Plateforme licence (D-14) : conception détaillée
|
||||||
|
|
||||||
|
> Sous-plan de l'Agent C. **Lecture seule** sur le code existant. Aucune ligne de
|
||||||
|
> `license.py` ni de `platform/` n'est écrite ici : ce document est la spec qui
|
||||||
|
> sera codée APRÈS le GO bêta (D-16), Phase 1.1 puis 1.2.
|
||||||
|
>
|
||||||
|
> Cadre D-14 **respecté à la lettre** (FastAPI + PostgreSQL + HTMX/Jinja2, OVH HDS,
|
||||||
|
> `app.aivanov.fr`, fastapi-users, Brevo, RSA-PSS 2048 + SHA256, `license.dat` DPAPI,
|
||||||
|
> phone home ≤ 30 j, 1 licence = 1 poste, grace 15 j, offline 30 j, révocation au check).
|
||||||
|
|
||||||
|
## 0. Ancrage dans l'existant (vérifié, read-only)
|
||||||
|
|
||||||
|
- Pas de `license.py` ni de `platform/` aujourd'hui → **fichiers/dossiers 100 % neufs**,
|
||||||
|
zéro conflit avec le moteur (`anonymizer_core_refactored_onnx.py`) ou la GUI.
|
||||||
|
- `cryptography==41.0.7` **déjà installée** → pas de nouvelle dépendance lourde côté client
|
||||||
|
(RSA-PSS/SHA256 fournis par `cryptography.hazmat`). Aucun ajout au risque
|
||||||
|
`numpy<2.0` / `gliner==0.2.18`.
|
||||||
|
- Le `.spec` PyInstaller bundle déjà `config/` → la **clé publique** s'embarque
|
||||||
|
naturellement comme `config/license_pubkey.pem` (ajout d'une seule ligne `datas`
|
||||||
|
au moment du codage, pas maintenant).
|
||||||
|
- `admin_mode.py` fournit le patron `is_admin()` / `admin_required()` et
|
||||||
|
`_project_root()` (résolution `sys._MEIPASS` en frozen) → `license.py` réutilise la
|
||||||
|
même logique de résolution de chemins en mode EXE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Architecture serveur (`platform/`, Phase 1.2 ~50h)
|
||||||
|
|
||||||
|
### 1.1 Arborescence du nouveau dossier (repo séparé ou sous-dossier `platform/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
platform/
|
||||||
|
├── app/
|
||||||
|
│ ├── main.py # FastAPI app + routers
|
||||||
|
│ ├── config.py # settings (env: DB URL, Brevo key, clé privée path)
|
||||||
|
│ ├── db.py # SQLAlchemy async engine + session
|
||||||
|
│ ├── models.py # tables ORM (clients, licences, postes, activations, revocations)
|
||||||
|
│ ├── auth.py # fastapi-users (UserManager, JWT/cookie)
|
||||||
|
│ ├── crypto/
|
||||||
|
│ │ ├── signer.py # signature RSA-PSS d'une licence (clé PRIVÉE, serveur only)
|
||||||
|
│ │ └── private_key.pem # JAMAIS commité (.gitignore + secret CI) — monté via volume OVH
|
||||||
|
│ ├── routers/
|
||||||
|
│ │ ├── pages.py # pages HTMX/Jinja2 (login, mes licences, activation, DL)
|
||||||
|
│ │ ├── api_client.py # endpoints appelés par l'EXE (/activate, /check, /download)
|
||||||
|
│ │ └── api_admin.py # endpoints admin Dom (create licence, revoke, parc)
|
||||||
|
│ ├── services/
|
||||||
|
│ │ ├── licensing.py # logique métier : 1 licence=1 poste, expiration, grace
|
||||||
|
│ │ ├── activation.py # bind machine_id ↔ licence, anti-réactivation
|
||||||
|
│ │ └── email.py # Brevo (activation, renouvellement, expiration J-30/J-7)
|
||||||
|
│ ├── templates/ # Jinja2 + fragments HTMX
|
||||||
|
│ └── static/
|
||||||
|
├── migrations/ # Alembic
|
||||||
|
├── tests/
|
||||||
|
├── Caddyfile # reverse proxy + Let's Encrypt (app.aivanov.fr)
|
||||||
|
├── docker-compose.yml # api + postgres + caddy
|
||||||
|
└── .github/workflows/deploy.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Schéma DB PostgreSQL
|
||||||
|
|
||||||
|
Cinq tables. fastapi-users gère `users` (= comptes de connexion). Le **client métier**
|
||||||
|
(`clients`) est distinct du `user` pour autoriser plusieurs comptes par organisation
|
||||||
|
plus tard, mais en MVP `user ↔ client` est 1:1.
|
||||||
|
|
||||||
|
```
|
||||||
|
users (géré par fastapi-users)
|
||||||
|
id UUID PK
|
||||||
|
email TEXT UNIQUE
|
||||||
|
hashed_password TEXT
|
||||||
|
is_active BOOL
|
||||||
|
is_superuser BOOL -- Dom = superuser (back-office)
|
||||||
|
is_verified BOOL
|
||||||
|
|
||||||
|
clients -- l'organisation cliente (hôpital, cabinet)
|
||||||
|
id UUID PK
|
||||||
|
user_id UUID FK→users.id (1:1 en MVP)
|
||||||
|
raison_sociale TEXT
|
||||||
|
finess TEXT NULL -- optionnel, cohérent métier santé
|
||||||
|
contact_email TEXT
|
||||||
|
created_at TIMESTAMPTZ
|
||||||
|
|
||||||
|
licences -- 1 abonnement annuel = N postes achetés
|
||||||
|
id UUID PK
|
||||||
|
client_id UUID FK→clients.id
|
||||||
|
ref TEXT UNIQUE -- ex. LIC-2026-000123 (humain)
|
||||||
|
postes_max INT -- nb de postes autorisés (souvent 1)
|
||||||
|
version_max TEXT NULL -- version max couverte par l'abo (ex "11.x")
|
||||||
|
issued_at TIMESTAMPTZ
|
||||||
|
expires_at TIMESTAMPTZ -- date de fin d'abonnement annuel
|
||||||
|
status ENUM(active, suspended, expired, cancelled)
|
||||||
|
created_at TIMESTAMPTZ
|
||||||
|
|
||||||
|
postes -- 1 ligne = 1 machine_id activée sous une licence
|
||||||
|
id UUID PK
|
||||||
|
licence_id UUID FK→licences.id
|
||||||
|
machine_id TEXT -- empreinte poste (voir §3.2)
|
||||||
|
label TEXT NULL -- nom donné par le client ("Poste accueil")
|
||||||
|
os_info TEXT NULL -- diag (Windows build), non PII
|
||||||
|
activated_at TIMESTAMPTZ
|
||||||
|
last_seen_at TIMESTAMPTZ -- dernier phone home réussi
|
||||||
|
status ENUM(active, revoked)
|
||||||
|
UNIQUE(licence_id, machine_id) -- 1 machine ne s'active qu'une fois/licence
|
||||||
|
-- contrainte applicative : COUNT(active) ≤ licences.postes_max
|
||||||
|
|
||||||
|
activations -- journal d'audit (immuable, append-only)
|
||||||
|
id UUID PK
|
||||||
|
poste_id UUID FK→postes.id NULL
|
||||||
|
licence_id UUID FK→licences.id
|
||||||
|
machine_id TEXT
|
||||||
|
event ENUM(activate, check, refuse_quota, refuse_revoked, revoke, renew)
|
||||||
|
ip INET NULL
|
||||||
|
user_agent TEXT NULL
|
||||||
|
detail JSONB NULL
|
||||||
|
created_at TIMESTAMPTZ
|
||||||
|
```
|
||||||
|
|
||||||
|
**Règle "1 licence = 1 poste" (D-14)** : implémentée par `postes_max` (défaut 1) +
|
||||||
|
contrainte applicative dans `services/licensing.py` : refus d'activation si
|
||||||
|
`COUNT(postes WHERE status=active) >= postes_max`. La table reste générique (permet
|
||||||
|
un futur multi-postes) sans casser le modèle MVP.
|
||||||
|
|
||||||
|
**Révocation au prochain check (D-14)** : `postes.status = revoked` → le `/check`
|
||||||
|
suivant renvoie `revoked`, l'EXE supprime son cache et repasse non-licencié. Pas de
|
||||||
|
push, pas de connexion permanente requise.
|
||||||
|
|
||||||
|
### 1.3 Endpoints FastAPI
|
||||||
|
|
||||||
|
**API client (appelés par l'EXE) — `routers/api_client.py`**
|
||||||
|
|
||||||
|
| Méthode | Route | Auth | Rôle |
|
||||||
|
|---|---|---|---|
|
||||||
|
| POST | `/api/v1/activate` | token client (clé licence + email/mdp ou jeton d'activation) | Lie `machine_id` à la licence, renvoie la **licence signée** (§4) |
|
||||||
|
| POST | `/api/v1/check` | machine_id + ref licence | Phone home : renvoie statut (active/expired/grace/revoked) + éventuelle licence re-signée (renouvellement) |
|
||||||
|
| GET | `/api/v1/download/{version}` | session client | Téléchargement de l'EXE (remplace OwnCloud) |
|
||||||
|
| GET | `/api/v1/version` | public | Dernière version dispo (pour notif maj) |
|
||||||
|
|
||||||
|
**Pages HTMX (humain) — `routers/pages.py`**
|
||||||
|
|
||||||
|
| Route | Page |
|
||||||
|
|---|---|
|
||||||
|
| `GET /` `GET /login` | Connexion (fastapi-users, cookie) |
|
||||||
|
| `GET /licences` | « Mes licences » : liste, expiration, postes consommés/max |
|
||||||
|
| `POST /licences/{id}/activate-token` (HTMX) | Génère un **jeton d'activation à usage unique** à coller dans l'EXE |
|
||||||
|
| `GET /licences/{id}/postes` (HTMX fragment) | Liste des postes activés, bouton « révoquer » |
|
||||||
|
| `POST /postes/{id}/revoke` (HTMX) | Passe le poste en `revoked` (effectif au prochain check) |
|
||||||
|
| `GET /download` | Page de téléchargement + checksum |
|
||||||
|
|
||||||
|
**API admin (Dom, superuser) — `routers/api_admin.py`**
|
||||||
|
|
||||||
|
| Route | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| `POST /admin/clients` | Créer un client + compte |
|
||||||
|
| `POST /admin/licences` | Émettre une licence (postes_max, expires_at) |
|
||||||
|
| `POST /admin/licences/{id}/renew` | Prolonger d'un an |
|
||||||
|
| `POST /admin/licences/{id}/cancel` | Suspendre/annuler |
|
||||||
|
| `GET /admin/parc` | Vue parc : clients, licences, postes, last_seen |
|
||||||
|
|
||||||
|
### 1.4 Pages HTMX (UX MVP, Phase 1.2)
|
||||||
|
|
||||||
|
- **Login** (fastapi-users, cookie session) → redirige vers `/licences`.
|
||||||
|
- **Mes licences** : carte par licence (réf, statut, expiration, jauge postes
|
||||||
|
`2/3`), bouton « Activer un poste » qui ouvre un fragment HTMX affichant le
|
||||||
|
**jeton d'activation** (copier-coller dans l'EXE).
|
||||||
|
- **Postes** : tableau (label, machine_id tronqué, last_seen, statut) + révoquer.
|
||||||
|
- **Téléchargement** : dernier EXE + checksum SHA256.
|
||||||
|
- Back-office Dom (superuser) : parc global + actions admin.
|
||||||
|
|
||||||
|
> HTMX = fragments HTML renvoyés par FastAPI, zéro SPA, déploiement simple (D-14).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Module client `license.py` (Phase 1.1 ~12h, fichier neuf)
|
||||||
|
|
||||||
|
### 2.1 Principe
|
||||||
|
|
||||||
|
`license.py` est **autonome** : il ne dépend que de `cryptography` (déjà présente) et
|
||||||
|
de la lib standard. Il n'importe NI le moteur NI la GUI → testable seul, zéro conflit.
|
||||||
|
La GUI (Agent A) ne fait qu'appeler son **API publique de statut** (§7).
|
||||||
|
|
||||||
|
### 2.2 Interface publique (contrat figé exposé à la GUI)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# --- Types ---
|
||||||
|
class LicenseState(Enum):
|
||||||
|
ACTIVE # licence valide, dans la période
|
||||||
|
GRACE # expirée mais < 15 j → mode dégradé autorisé
|
||||||
|
EXPIRED # > 15 j après expiration → bloquant (sauf bêta)
|
||||||
|
OFFLINE_STALE # pas de phone home depuis > 30 j → exige reconnexion
|
||||||
|
REVOKED # révoquée côté serveur
|
||||||
|
UNLICENSED # aucune licence (ex. bêta, ou avant activation)
|
||||||
|
INVALID # signature falsifiée / fichier corrompu / machine_id divergent
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LicenseStatus:
|
||||||
|
state: LicenseState
|
||||||
|
client_id: str | None
|
||||||
|
expires_at: datetime | None
|
||||||
|
days_remaining: int | None # négatif si en grace
|
||||||
|
last_check_at: datetime | None
|
||||||
|
machine_id: str
|
||||||
|
message_fr: str # texte prêt pour la bannière GUI
|
||||||
|
can_anonymize: bool # ACTIVE et GRACE → True ; sinon False (hors bêta)
|
||||||
|
|
||||||
|
# --- API que la GUI appelle ---
|
||||||
|
def get_status(force_refresh: bool = False) -> LicenseStatus: ...
|
||||||
|
# Lit license.dat (cache), valide signature + machine_id + dates SANS réseau.
|
||||||
|
# Si dernier check > 30 j → tente un phone home ; sinon reste offline.
|
||||||
|
|
||||||
|
def activate(license_token: str) -> LicenseStatus: ...
|
||||||
|
# Appelle POST /activate, reçoit la licence signée, la chiffre dans license.dat.
|
||||||
|
|
||||||
|
def check_now() -> LicenseStatus: ...
|
||||||
|
# Force un phone home (POST /check) ; met à jour last_check_at + re-signature.
|
||||||
|
|
||||||
|
def deactivate() -> None: ...
|
||||||
|
# Supprime license.dat local (libère le poste après révocation côté serveur).
|
||||||
|
|
||||||
|
def is_beta_build() -> bool: ...
|
||||||
|
# True si BETA (pas de licence) → court-circuite tout (Phase 0 Réunion).
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Phase 0 / bêta** : `is_beta_build()` renvoie True (flag de build), `get_status()`
|
||||||
|
> renvoie `UNLICENSED` avec `can_anonymize=True`. Aucun appel réseau, aucun blocage.
|
||||||
|
> C'est le mode livré au testeur Réunion (D-14, Phase 0).
|
||||||
|
|
||||||
|
### 2.3 Algo de vérification RSA-PSS (offline, cœur de la sécu)
|
||||||
|
|
||||||
|
```
|
||||||
|
verify(license_json, signature, pubkey):
|
||||||
|
1. Recomposer le payload canonique = json.dumps(license_obj, sort_keys=True,
|
||||||
|
separators=(',',':')) encodé UTF-8 # canonicalisation déterministe obligatoire
|
||||||
|
2. public_key.verify(
|
||||||
|
signature, # bytes (base64-décodés)
|
||||||
|
canonical_payload,
|
||||||
|
padding.PSS(mgf=MGF1(SHA256()), salt_length=PSS.MAX_LENGTH),
|
||||||
|
SHA256())
|
||||||
|
3. Si InvalidSignature → state = INVALID (refus)
|
||||||
|
4. Vérifier machine_id(payload) == machine_id(local) # anti-recopie sur autre PC
|
||||||
|
5. Vérifier version couverte (payload.version_max ≥ version courante si présent)
|
||||||
|
6. Calcul d'état temporel (now vs expires_at, last_check) → ACTIVE/GRACE/EXPIRED/...
|
||||||
|
```
|
||||||
|
|
||||||
|
Clé publique embarquée : `config/license_pubkey.pem` (PEM SubjectPublicKeyInfo,
|
||||||
|
RSA 2048). Clé privée : **jamais** dans le repo client, uniquement sur OVH
|
||||||
|
(`platform/app/crypto/private_key.pem`, monté en volume/secret CI).
|
||||||
|
|
||||||
|
### 2.4 `machine_id` (empreinte poste, §3.2 pour le flow)
|
||||||
|
|
||||||
|
```
|
||||||
|
machine_id = SHA256( os_uuid || cpu_id || mac_primaire )[:32] # hex tronqué
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Windows** : `MachineGuid` (registre `HKLM\SOFTWARE\Microsoft\Cryptography`) +
|
||||||
|
`wmic csproduct uuid` + 1ʳᵉ MAC non virtuelle.
|
||||||
|
- **Linux/Mac** (dev/tests) : `/etc/machine-id` + MAC.
|
||||||
|
- Hashé (SHA256 tronqué) → **non réversible**, pas un identifiant PII brut.
|
||||||
|
- Tolérance : on hashe des composants stables ; pas le n° de disque (changé au
|
||||||
|
reformatage). Si dérive (carte réseau changée) → `INVALID` → ré-activation
|
||||||
|
nécessaire (cas rare, géré par le support).
|
||||||
|
|
||||||
|
### 2.5 Cache local chiffré `license.dat`
|
||||||
|
|
||||||
|
- Contenu : la licence signée (JSON+signature) **+** métadonnées locales
|
||||||
|
(`last_check_at`, `machine_id`).
|
||||||
|
- **Windows** : chiffré via **DPAPI** (`win32crypt.CryptProtectData`, scope
|
||||||
|
`CRYPTPROTECT_LOCAL_MACHINE`) → déchiffrable seulement sur ce poste/compte.
|
||||||
|
- **Linux/Mac** : chiffrement symétrique simple (Fernet) avec clé dérivée du
|
||||||
|
`machine_id` (suffisant hors prod Windows ; D-14 dit « chiffré simple »).
|
||||||
|
- Emplacement : à côté de l'EXE en frozen (réutilise `_project_root()` du patron
|
||||||
|
`admin_mode.py`), `%LOCALAPPDATA%\Aivanov\Anonymisation\license.dat` recommandé
|
||||||
|
pour survivre aux mises à jour.
|
||||||
|
- **Anti-rollback horloge** : on stocke `last_check_at` ET on refuse un `now` <
|
||||||
|
`last_check_at` (recul d'horloge) → bascule `OFFLINE_STALE` plutôt que prolonger
|
||||||
|
frauduleusement la grace.
|
||||||
|
|
||||||
|
### 2.6 Logique grace / offline / révocation (machine à états)
|
||||||
|
|
||||||
|
```
|
||||||
|
À get_status():
|
||||||
|
charger+vérifier license.dat
|
||||||
|
si INVALID/REVOKED/absent → état correspondant (can_anonymize=False, sauf bêta)
|
||||||
|
sinon:
|
||||||
|
age_check = now - last_check_at
|
||||||
|
si age_check > 30 j → tenter check_now()
|
||||||
|
succès → repartir avec licence fraîche
|
||||||
|
échec réseau → état OFFLINE_STALE (can_anonymize=False : exige reconnexion)
|
||||||
|
calc temporel:
|
||||||
|
now <= expires_at → ACTIVE (can_anonymize=True)
|
||||||
|
expires_at < now <= expires_at+15 j → GRACE (can_anonymize=True, bannière)
|
||||||
|
now > expires_at+15 j → EXPIRED (can_anonymize=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Grace 15 j** : `can_anonymize=True` + `message_fr` = « Licence expirée — pensez à
|
||||||
|
renouveler (J-X) ». Mode dégradé = juste la bannière (D-14), le moteur ne change pas.
|
||||||
|
- **Offline 30 j** : tant que `age_check ≤ 30 j`, **aucun réseau requis** (full offline).
|
||||||
|
Au-delà, un phone home est exigé ; s'il échoue → `OFFLINE_STALE` bloquant jusqu'à
|
||||||
|
reconnexion (évite usage illimité hors-ligne).
|
||||||
|
- **Révocation** : détectée au `/check` (serveur renvoie `revoked`) → `deactivate()`
|
||||||
|
local → `REVOKED`. Pas instantané par design (D-14), effectif au prochain check.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Format exact de la licence signée
|
||||||
|
|
||||||
|
### 3.1 Objet JSON (payload signé)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"license_ref": "LIC-2026-000123",
|
||||||
|
"client_id": "5f3a...uuid",
|
||||||
|
"machine_id": "9b1c2d...32hex",
|
||||||
|
"issued_at": "2026-06-10T09:00:00Z",
|
||||||
|
"expires_at": "2027-06-10T09:00:00Z",
|
||||||
|
"version_max": "11.x",
|
||||||
|
"grace_days": 15,
|
||||||
|
"offline_max_days": 30
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Canonicalisation **obligatoire** avant signature ET vérification :
|
||||||
|
> `json.dumps(payload, sort_keys=True, separators=(',',':'))` → bytes UTF-8.
|
||||||
|
> Tout écart de sérialisation invalide la signature.
|
||||||
|
|
||||||
|
### 3.2 Enveloppe stockée / transmise
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"payload": { ... l'objet ci-dessus ... },
|
||||||
|
"signature": "base64( RSA-PSS-SHA256( canonical(payload) ) )",
|
||||||
|
"alg": "RSASSA-PSS-SHA256",
|
||||||
|
"key_id": "aivanov-license-2026"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`key_id` permet une **rotation de clé** future (le client embarque plusieurs pubkeys
|
||||||
|
indexées par `key_id`). MVP : une seule clé.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Flows
|
||||||
|
|
||||||
|
### 4.1 Activation d'un poste
|
||||||
|
```
|
||||||
|
Client se connecte sur app.aivanov.fr → /licences → « Activer un poste »
|
||||||
|
→ serveur génère jeton d'activation usage unique (lié licence_id)
|
||||||
|
Client lance l'EXE → saisit le jeton → license.py.activate(token)
|
||||||
|
→ POST /activate { token, machine_id, os_info }
|
||||||
|
→ serveur : vérifie quota (COUNT active < postes_max), crée poste, journalise
|
||||||
|
→ serveur signe la licence (clé privée) et renvoie l'enveloppe
|
||||||
|
→ license.py chiffre l'enveloppe dans license.dat (DPAPI), state=ACTIVE
|
||||||
|
Refus si quota atteint → 409 refuse_quota → message GUI « postes max atteints ».
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Expiration + grace period
|
||||||
|
```
|
||||||
|
Au lancement, get_status() (offline) :
|
||||||
|
now <= expires_at → ACTIVE
|
||||||
|
J0..J15 après expires_at → GRACE : anonymisation OK + bannière jaune « J-X »
|
||||||
|
> J15 → EXPIRED : anonymisation bloquée, CTA « renouveler »
|
||||||
|
Renouvellement : Dom renew côté serveur → au prochain /check, licence re-signée
|
||||||
|
avec nouveau expires_at → state repasse ACTIVE automatiquement.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Offline 30 jours
|
||||||
|
```
|
||||||
|
Poste sans réseau :
|
||||||
|
age_check <= 30 j → fonctionne 100 % offline (vérif locale signature+dates)
|
||||||
|
age_check > 30 j → tente /check ; si échec → OFFLINE_STALE (bloquant)
|
||||||
|
message « Connexion requise pour valider la licence ».
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Révocation
|
||||||
|
```
|
||||||
|
Dom (ou client) clique « révoquer » → postes.status=revoked (audit logged).
|
||||||
|
Effet : rien d'immédiat sur le poste (offline).
|
||||||
|
Au prochain /check du poste (≤ 30 j) → serveur renvoie revoked
|
||||||
|
→ license.py.deactivate() supprime license.dat → state=REVOKED (bloquant).
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Plan de branches et livrables
|
||||||
|
|
||||||
|
> **Tout démarre APRÈS le GO bêta (D-16).** Branches créées depuis la branche de
|
||||||
|
> livraison figée, conformément au plan maître §6.
|
||||||
|
|
||||||
|
### Phase 1.1 — client `license.py` (~12h) — EN PREMIER (le plus isolé)
|
||||||
|
- **Branche** : `feature/v11-5-license-client`
|
||||||
|
- **Livrables** :
|
||||||
|
- `license.py` (module neuf, API §2.2)
|
||||||
|
- `config/license_pubkey.pem` (clé publique de test d'abord, prod ensuite)
|
||||||
|
- 1 ligne ajoutée au `.spec` (`("config/license_pubkey.pem", "config")`) — ajout
|
||||||
|
isolé, ne touche aucune entrée existante
|
||||||
|
- flag de build `BETA` (dans `build_info.py`) pour `is_beta_build()`
|
||||||
|
- `tests/unit/test_license.py` (§6)
|
||||||
|
- **Isolation** : `license.py` n'importe ni le moteur ni la GUI → **zéro conflit**.
|
||||||
|
Mergeable indépendamment (plan maître §6.2).
|
||||||
|
|
||||||
|
### Phase 1.2 — plateforme `platform/` (~50h) — APRÈS, en parallèle de A/B
|
||||||
|
- **Branche** : `feature/v11-5-platform` (ou repo `platform/` dédié)
|
||||||
|
- **Livrables** : arborescence §1.1, migrations Alembic, docker-compose,
|
||||||
|
Caddyfile (`app.aivanov.fr`), workflow GitHub Actions, `tests/` serveur.
|
||||||
|
- **Isolation** : dossier `platform/` entièrement neuf → **zéro fichier applicatif
|
||||||
|
partagé** avec moteur/GUI/admin.
|
||||||
|
|
||||||
|
### Ce qui est isolé (résumé anti-collision pour Agent D)
|
||||||
|
| Zone Agent C | Conflit possible ? |
|
||||||
|
|---|---|
|
||||||
|
| `license.py` (neuf) | Non |
|
||||||
|
| `platform/` (neuf) | Non |
|
||||||
|
| `config/license_pubkey.pem` (neuf) | Non |
|
||||||
|
| `.spec` (+1 entrée datas) | Quasi nul (ajout en fin de liste) |
|
||||||
|
| `build_info.py` (+1 flag BETA) | Faible (1 constante) — à coordonner avec D |
|
||||||
|
| Point d'appel GUI | **Contrat §7** — A réserve l'emplacement, C fournit l'API |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Tests attendus
|
||||||
|
|
||||||
|
### Client `license.py` (`tests/unit/test_license.py`) — pas de mock réseau pour la crypto
|
||||||
|
| Test | Attendu |
|
||||||
|
|---|---|
|
||||||
|
| Signature valide | licence signée avec la clé privée de test → `ACTIVE` |
|
||||||
|
| Signature **falsifiée** (1 octet modifié dans payload OU signature) | `INVALID`, `can_anonymize=False` |
|
||||||
|
| `machine_id` divergent (licence d'un autre poste) | `INVALID` |
|
||||||
|
| Expiration : now < expires | `ACTIVE` |
|
||||||
|
| Grace : expires < now ≤ +15 j | `GRACE`, `can_anonymize=True`, `days_remaining` négatif |
|
||||||
|
| Au-delà grace : now > +15 j | `EXPIRED`, `can_anonymize=False` |
|
||||||
|
| Offline ≤ 30 j (pas de réseau) | reste `ACTIVE`/`GRACE` sans appel réseau |
|
||||||
|
| Offline > 30 j + check échoue | `OFFLINE_STALE`, bloquant |
|
||||||
|
| Révocation (check renvoie revoked) | `REVOKED`, `license.dat` supprimé |
|
||||||
|
| Recul d'horloge (now < last_check) | pas de prolongation frauduleuse → `OFFLINE_STALE` |
|
||||||
|
| Cache corrompu | `INVALID` sans crash |
|
||||||
|
| Mode bêta (`is_beta_build()`) | `UNLICENSED` + `can_anonymize=True`, zéro réseau |
|
||||||
|
|
||||||
|
> Fixtures : on génère une **paire RSA de test** dans la fixture (jamais la clé prod),
|
||||||
|
> on signe des payloads à la volée → tests déterministes, hermétiques, sans serveur.
|
||||||
|
|
||||||
|
### Serveur `platform/tests/`
|
||||||
|
| Test | Attendu |
|
||||||
|
|---|---|
|
||||||
|
| Activation poste | crée `postes`, renvoie licence signée vérifiable par la pubkey |
|
||||||
|
| Quota 1 licence = 1 poste | 2ᵉ activation sur `postes_max=1` → `409 refuse_quota` |
|
||||||
|
| Réactivation même machine_id | idempotent (pas de doublon) |
|
||||||
|
| `/check` poste révoqué | renvoie `revoked` |
|
||||||
|
| Renew | `expires_at` prolongé → licence re-signée |
|
||||||
|
| Auth | endpoints admin refusés aux non-superusers |
|
||||||
|
| Audit | chaque événement écrit une ligne `activations` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Zone de contact avec Agent A (GUI v6)
|
||||||
|
|
||||||
|
L'Agent A réserve un emplacement UI (bannière d'état licence). **C fournit l'API**,
|
||||||
|
A ne fait que l'afficher. Contrat figé :
|
||||||
|
|
||||||
|
```python
|
||||||
|
from license import get_status, LicenseState
|
||||||
|
|
||||||
|
st = get_status() # jamais bloquant, pas de réseau sauf si >30 j
|
||||||
|
banner_text = st.message_fr # ex. « Licence active — expire le 10/06/2027 »
|
||||||
|
banner_level = { # pour la couleur de bannière
|
||||||
|
LicenseState.ACTIVE: "ok", # vert/neutre
|
||||||
|
LicenseState.GRACE: "warning", # jaune « renouveler J-X »
|
||||||
|
LicenseState.EXPIRED: "error", # rouge bloquant
|
||||||
|
LicenseState.OFFLINE_STALE: "warning", # « connexion requise »
|
||||||
|
LicenseState.REVOKED: "error",
|
||||||
|
LicenseState.UNLICENSED: "info", # bêta : info discrète ou rien
|
||||||
|
LicenseState.INVALID: "error",
|
||||||
|
}[st.state]
|
||||||
|
allow_run = st.can_anonymize # la GUI grise « Anonymiser » si False
|
||||||
|
```
|
||||||
|
|
||||||
|
- La GUI **n'appelle jamais** `/activate` ou `/check` directement : tout passe par
|
||||||
|
`license.py` (`activate(token)`, `check_now()`).
|
||||||
|
- L'écran d'activation (saisie du jeton) appelle `license.activate(token)` et affiche
|
||||||
|
le `LicenseStatus` retourné.
|
||||||
|
- En bêta, `get_status()` renvoie `UNLICENSED` + `can_anonymize=True` → A peut masquer
|
||||||
|
totalement la bannière (rien à afficher).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Risques & points à valider par Dom
|
||||||
|
|
||||||
|
| Point | Reco |
|
||||||
|
|---|---|
|
||||||
|
| Stabilité `machine_id` (carte réseau changée → ré-activation) | Hasher des composants stables (MachineGuid + UUID carte mère), pas le disque. Acceptable : support gère les rares dérives. |
|
||||||
|
| DPAPI `LOCAL_MACHINE` vs `CURRENT_USER` | `LOCAL_MACHINE` = tous les comptes du poste partagent la licence (cohérent « 1 poste »). À confirmer côté hôpital (sessions partagées). |
|
||||||
|
| `pywin32` (DPAPI) pas encore listé côté EXE | Ajout dépendance Windows uniquement, en Phase 1.1. Hors périmètre `numpy<2.0`. |
|
||||||
|
| Rotation de clé future | `key_id` prévu dans l'enveloppe (§3.2) → non bloquant. |
|
||||||
|
| Bêta sans licence | `is_beta_build()` court-circuite tout (D-14 Phase 0 respecté). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Résumé exécutif
|
||||||
|
|
||||||
|
Conception complète de la brique licence D-14, **respectant le cadre validé** sans le
|
||||||
|
réinventer. Côté serveur (`platform/`, Phase 1.2) : FastAPI + PostgreSQL (5 tables
|
||||||
|
clients/licences/postes/activations + users fastapi-users), endpoints client
|
||||||
|
(`/activate`, `/check`, `/download`) + pages HTMX (« Mes licences », activation,
|
||||||
|
révocation) + back-office Dom. Côté client (`license.py`, Phase 1.1) : module **neuf et
|
||||||
|
isolé** (zéro import moteur/GUI), API publique figée (`get_status`, `activate`,
|
||||||
|
`check_now`, `deactivate`), vérif **RSA-PSS 2048/SHA256 offline**, `machine_id` hashé,
|
||||||
|
cache `license.dat` **DPAPI**, machine à états grace 15 j / offline 30 j / révocation au
|
||||||
|
check. Format licence = JSON canonique signé + enveloppe base64 avec `key_id`. `cryptography`
|
||||||
|
**déjà installée** (aucune dépendance lourde nouvelle). Plan de branches : 1.1 client (mergeable
|
||||||
|
seul, le plus isolé) puis 1.2 plateforme. Tests crypto hermétiques (paire RSA de test) +
|
||||||
|
tests serveur (quota, révocation, renew, audit). Contrat GUI fourni à l'Agent A
|
||||||
|
(bannière via `message_fr` + `can_anonymize`). **Aucun code de prod écrit, aucun déploiement.**
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
---
|
||||||
|
from: claude (Agent D — intégration)
|
||||||
|
to: dom
|
||||||
|
date: 2026-06-05T18:40:00+02:00
|
||||||
|
topic: planD-integration-v11-5
|
||||||
|
status: open
|
||||||
|
priority: high
|
||||||
|
references:
|
||||||
|
- plan: docs/coordination/inbox/for-dom/2026-06-05_claude_plan-v11-5-parallele.md
|
||||||
|
- decision: docs/coordination/decisions/2026-06-05_dom_d17-v11-5-chantiers-paralleles.md
|
||||||
|
- decision: docs/coordination/decisions/2026-06-05_dom_d16-test-windows-avant-diffusion.md
|
||||||
|
- decision: docs/coordination/decisions/2026-06-02_dom_d14-plateforme-licence-architecture.md
|
||||||
|
- decision: docs/coordination/decisions/2026-06-02_dom_d13-partial-scope.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Plan D — Intégration / merge v11.5 (anti-collision 3 agents)
|
||||||
|
|
||||||
|
**Planification uniquement — lecture seule.** Aucun code modifié, aucun commit.
|
||||||
|
Ce document définit comment les chantiers A (GUI v6), B (D-13 complet) et
|
||||||
|
C (licence) s'intègrent sans se marcher dessus.
|
||||||
|
|
||||||
|
État vérifié au moment de la rédaction (HEAD `57aa0f0`, branche `feature/q1-quarantine-mvp`) :
|
||||||
|
- Suite `tests/unit` : **98 tests collectés** (baseline confirmée).
|
||||||
|
- `admin_mode.py` (2.4 ko) et `config_defaults.py` (5.8 ko) **existent déjà** → B étend, ne crée pas.
|
||||||
|
- `license.py`, `gui_v6/`, `platform/` : **n'existent pas encore** → créations propres (A, C).
|
||||||
|
- `Pseudonymisation_Gui_V5.py` (119 ko) : fichier de livraison bêta → **gelé**, sert de référence à A.
|
||||||
|
- `backup/windows-wip-2026-06-05` : **déjà poussé sur Gitea** (section 0 du plan maître close).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Frontières de fichiers (qui crée / modifie quoi)
|
||||||
|
|
||||||
|
### Agent A — GUI v6 (zone PROPRE)
|
||||||
|
| Fichier / dossier | Action | Note |
|
||||||
|
|---|---|---|
|
||||||
|
| `Pseudonymisation_Gui_V6.py` | **CRÉE** (neuf) | réécriture propre, pas de merge brut du WIP |
|
||||||
|
| `gui_v6/` (nouveau package) | **CRÉE** | onglets, widgets, thèmes, assets |
|
||||||
|
| `gui_v6/assets/`, thèmes | **CRÉE** | maquette v6 validée 2026-05-06 |
|
||||||
|
| `Pseudonymisation_Gui_V5.py` | **NE TOUCHE PAS** | reste le point d'entrée bêta jusqu'à bascule finale |
|
||||||
|
|
||||||
|
A consomme le moteur via l'API interne stable (mêmes signatures qu'en v5).
|
||||||
|
A **réserve deux emplacements UI** : (a) section « Paramètres avancés / Profils
|
||||||
|
techniques » pour B, (b) bannière état licence (statut/expiration) pour C.
|
||||||
|
|
||||||
|
### Agent B — D-13 complet (zone PROPRE + zone partagée avec A)
|
||||||
|
| Fichier / dossier | Action | Note |
|
||||||
|
|---|---|---|
|
||||||
|
| `admin_mode.py` | **ÉTEND** (existe déjà) | ajoute la matrice de réglages protégés, garde `is_admin()`/`admin_required()` |
|
||||||
|
| `config_defaults.py` | **ÉTEND** (existe déjà) | flags admin/non-admin par réglage |
|
||||||
|
| `gui_v6/` sections « avancé » | **CO-ÉCRIT avec A** | B = règles d'accès, A = écrans |
|
||||||
|
| moteur de détection (`anonymizer_core_*`) | **NE TOUCHE PAS** | garde-fou n°1 |
|
||||||
|
|
||||||
|
### Agent C — Licence (zone PROPRE, la plus isolée)
|
||||||
|
| Fichier / dossier | Action | Note |
|
||||||
|
|---|---|---|
|
||||||
|
| `license.py` | **CRÉE** (neuf) | client : vérif RSA-PSS, expiration, grace 15 j, offline 30 j, révocation |
|
||||||
|
| `platform/` (serveur) | **CRÉE** (neuf) | activation poste, 1 licence = 1 machine_id (D-14 phase 1.2) |
|
||||||
|
| clé **publique** embarquée | **CRÉE** | clé privée RSA **jamais** dans le repo client (serveur OVH uniquement) |
|
||||||
|
| GUI, core | **NE TOUCHE PAS** | C n'expose qu'une API statut/expiration consommée par A |
|
||||||
|
|
||||||
|
### Agent D — Intégration (ce document)
|
||||||
|
| Zone | Action |
|
||||||
|
|---|---|
|
||||||
|
| `docs/coordination/` | docs de merge, ce plan |
|
||||||
|
| `tests/` (structure, CI) | organisation des nouveaux dossiers de tests par chantier |
|
||||||
|
| code applicatif | **NE TOUCHE PAS** |
|
||||||
|
|
||||||
|
### Fichiers PARTAGÉS à risque (à surveiller en priorité)
|
||||||
|
1. **`gui_v6/` sections « avancé »** — seule vraie co-édition (A↔B). Mitigation :
|
||||||
|
contrat écrit A↔B avant tout code ; B livre une **interface de règles**
|
||||||
|
(`admin_mode.get_field_policy(field) -> visible|disabled|hidden`) que A appelle,
|
||||||
|
plutôt que B éditant les écrans directement.
|
||||||
|
2. **`Pseudonymisation_Gui_V6.py`** — propriété exclusive A. B et C n'y écrivent pas ;
|
||||||
|
ils exposent des fonctions, A les branche.
|
||||||
|
3. **`requirements.txt` / `.spec` PyInstaller** — touchés par C (dépendances RSA :
|
||||||
|
`cryptography`) et par le build final. Mitigation : un **seul** agent (D, au merge)
|
||||||
|
consolide `requirements.txt` et le `.spec` ; A/B/C déposent leurs deltas de deps en doc.
|
||||||
|
4. **`config_defaults.py`** — B l'étend. A le lit seulement. Pas d'écriture concurrente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Dépendances entre agents
|
||||||
|
|
||||||
|
```
|
||||||
|
C (licence) ──API statut/expiration──► A (GUI v6, bannière licence)
|
||||||
|
B (D-13) ──API get_field_policy────► A (GUI v6, écrans avancés)
|
||||||
|
A (GUI v6) ──écrans + emplacements───► B se greffe dessus
|
||||||
|
```
|
||||||
|
|
||||||
|
- **B dépend de A** pour les écrans : B ne peut finaliser ses sections « avancé »
|
||||||
|
qu'une fois la structure d'onglets v6 posée par A. B peut **démarrer en parallèle**
|
||||||
|
sur la logique pure (matrice de règles dans `admin_mode.py` + tests headless),
|
||||||
|
puis brancher l'UI quand A a livré.
|
||||||
|
- **A dépend de C** pour l'API licence (statut/expiration). A peut avancer avec un
|
||||||
|
**stub** d'interface licence (contrat figé) tant que C n'a pas fini, puis brancher
|
||||||
|
le vrai `license.py`.
|
||||||
|
- **C ne dépend de personne** → chantier le plus isolé, mergeable en premier.
|
||||||
|
- **D dépend de tous** (validation finale).
|
||||||
|
|
||||||
|
Règle d'or : chaque dépendance passe par une **interface contractualisée** (signature
|
||||||
|
de fonction figée tôt), pas par un partage de fichier. Cela permet le parallélisme.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Ordre de merge recommandé (et justification)
|
||||||
|
|
||||||
|
Confirme la proposition §6 du plan maître :
|
||||||
|
|
||||||
|
1. **Base** : après **GO bêta** (D-16), figer la branche de livraison
|
||||||
|
(`feature/q1-quarantine-mvp` à `15f73f8` ou le hotfix éventuel), puis créer
|
||||||
|
`feature/v11-5` **depuis cette base figée**.
|
||||||
|
2. **C (licence) en premier** — *le plus isolé* : `license.py` + `platform/` neufs,
|
||||||
|
zéro conflit moteur/GUI. Mergeable seul, testable seul. Réduit le risque tôt.
|
||||||
|
3. **A (GUI v6) ensuite** — gros morceau, fichier neuf `Pseudonymisation_Gui_V6.py`.
|
||||||
|
Branche la bannière licence sur l'API de C (déjà mergée). Pas de conflit avec C
|
||||||
|
(surfaces disjointes).
|
||||||
|
4. **B (D-13) en dernier** — se *greffe sur A* (sections avancées de la GUI v6).
|
||||||
|
Merge après A pour que les écrans existent. La logique `admin_mode.py` étant déjà
|
||||||
|
prête et testée headless, le merge B = branchement UI + tests matrice.
|
||||||
|
5. **Validation D** — qualité + tests + build, puis bascule de v6 par défaut
|
||||||
|
(changement du point d'entrée v5→v6) en **dernier commit**, isolé et réversible.
|
||||||
|
|
||||||
|
Justification : on merge du moins couplé au plus couplé. C isolé d'abord retire le
|
||||||
|
risque cryptographique tôt ; A pose le squelette UI dont B a besoin ; B greffé en
|
||||||
|
dernier minimise la fenêtre de co-édition. La bascule v5→v6 est le tout dernier pas,
|
||||||
|
trivialement réversible (revert d'un seul commit).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Critères d'acceptation v11.5 (gate de merge)
|
||||||
|
|
||||||
|
Aucun merge dans `feature/v11-5` n'est accepté sans :
|
||||||
|
|
||||||
|
| Critère | Cible | Vérification |
|
||||||
|
|---|---|---|
|
||||||
|
| Non-régression moteur | `tests/unit` **98 passed** (inchangé) | `pytest tests/unit -q` |
|
||||||
|
| Leak score | **100/100** inchangé | `tests/unit/test_leak_scanner.py` + audit_30 |
|
||||||
|
| Audit qualité | `evaluate_quality.py` **≥ 98.5** (baseline) | `scripts/evaluate_quality.py` |
|
||||||
|
| Build EXE | **reproductible** (PyInstaller --onefile, config externe) | build Windows 192.168.1.11 |
|
||||||
|
| GUI v6 (A) | smoke lancement + workflow principal OK ; contrat moteur identique v5 | tests `gui_batch_paths`, `manual_masking` conservés |
|
||||||
|
| D-13 (B) | chaque réglage protégé caché/désactivé en non-admin ; `admin_required` lève ; sauvegarde config sensible bloquée non-admin | nouveaux tests matrice admin |
|
||||||
|
| Licence (C) | signature RSA-PSS (valide/falsifiée), expiration, grace 15 j, offline 30 j, révocation ; serveur : 1 licence = 1 machine_id | nouveaux tests `license.py` + serveur |
|
||||||
|
|
||||||
|
**Garde-fou non négociable** : les 98 tests verts + leak 100/100 sont le filet.
|
||||||
|
Le moteur de détection ne bouge pas → tout chantier qui ferait baisser ces deux
|
||||||
|
chiffres est rejeté, point.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Stratégie de branches
|
||||||
|
|
||||||
|
```
|
||||||
|
feature/q1-quarantine-mvp (livraison bêta — GELÉE jusqu'au GO Dom)
|
||||||
|
│ ◄── hotfix MVP éventuel possible ICI uniquement (D-16)
|
||||||
|
│
|
||||||
|
[GO BÊTA] → tag de livraison figé (ex: beta-v11)
|
||||||
|
│
|
||||||
|
└──► feature/v11-5 (créée APRÈS GO, depuis la base figée)
|
||||||
|
├── feat/v11-5-licence (C) → merge 1
|
||||||
|
├── feat/v11-5-gui-v6 (A) → merge 2
|
||||||
|
└── feat/v11-5-d13 (B) → merge 3 (greffé sur A)
|
||||||
|
```
|
||||||
|
|
||||||
|
Règles :
|
||||||
|
- **Aucune branche v11.5 créée avant le GO bêta** (gel D-16/D-17).
|
||||||
|
- `feature/v11-5` part de la **base figée** (tag de livraison), pas de `main` ni
|
||||||
|
d'une branche en mouvement.
|
||||||
|
- Sous-branches par chantier, merge dans `feature/v11-5` dans l'ordre §3.
|
||||||
|
- Hotfix MVP, s'il survient pendant la bêta, reste sur `feature/q1-quarantine-mvp`
|
||||||
|
et sera **rebasé/cherry-pické** dans la base figée avant création de `feature/v11-5`
|
||||||
|
(ne jamais mélanger hotfix et refonte).
|
||||||
|
- Tag de sécurité conservé sur `backup/windows-wip-2026-06-05` (anti-gc).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Risques principaux + mitigations
|
||||||
|
|
||||||
|
| Risque | Impact | Mitigation |
|
||||||
|
|---|---|---|
|
||||||
|
| GUI v6 casse le moteur | leak/qualité régressent | Contrat moteur strict (mêmes I/O que v5) + 98 tests verts obligatoires au merge |
|
||||||
|
| Co-édition A/B sur écrans avancés | conflits Git, double logique | Contrat écrit A↔B AVANT code ; B expose `get_field_policy()`, A consomme — pas de co-édition de fichier |
|
||||||
|
| Mélange hotfix MVP / v11.5 | divergence, régression bêta | Gel respecté ; v11.5 sur branche dédiée créée APRÈS GO ; hotfix cherry-pické proprement |
|
||||||
|
| Clé privée RSA fuit | licences forgeables | Clé privée **serveur OVH uniquement** (D-14) ; client n'embarque que la clé publique |
|
||||||
|
| `requirements.txt` / `.spec` édités par 3 agents | build cassé, conflits | Consolidation par **un seul** agent (D) au merge ; deltas de deps livrés en doc |
|
||||||
|
| WIP GUI v6 sur disque unique | perte de la base A | Déjà mitigé : backup poussé sur Gitea + tag anti-gc |
|
||||||
|
| Plateforme licence = ~50h | dérapage planning | Phasage D-14 : 1.1 client (~12h) avant 1.2 serveur (~50h) ; C livrable client d'abord |
|
||||||
|
| Bascule v5→v6 par défaut | régression point d'entrée | Bascule = dernier commit isolé, revert trivial |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Rappel garde-fous
|
||||||
|
|
||||||
|
- **GEL BÊTA** : rien ne démarre en *code* avant le GO de Dom (D-16/D-17). Seuls
|
||||||
|
plans, inventaires, contrats d'interface et docs sont produits maintenant.
|
||||||
|
- **Le moteur de détection ne bouge pas.** v11.5 = refonte UI + ajouts périphériques
|
||||||
|
(licence, admin). Le leak score 100/100 et les 98 tests sont intouchables.
|
||||||
|
- **Interfaces contractualisées** entre A/B/C : tout passe par des signatures de
|
||||||
|
fonctions figées tôt, jamais par du partage de fichier — c'est ce qui rend le
|
||||||
|
parallélisme sûr.
|
||||||
|
- **Un seul agent (D) consolide** `requirements.txt` et le `.spec` au merge.
|
||||||
|
|
||||||
|
— Claude (Agent D)
|
||||||
@@ -61,5 +61,8 @@ Format : `YYYY-MM-DD HH:MM | AUTEUR | ACTION | RÉSUMÉ`
|
|||||||
2026-06-05 12:35 | claude | EN ATTENTE Dom | Pack prêt sur 192.168.1.11:release\. Pas d'upload OwnCloud sans accord (action sortante). WIP Windows sauvegardé backup/windows-wip-2026-06-05.
|
2026-06-05 12:35 | claude | EN ATTENTE Dom | Pack prêt sur 192.168.1.11:release\. Pas d'upload OwnCloud sans accord (action sortante). WIP Windows sauvegardé backup/windows-wip-2026-06-05.
|
||||||
2026-06-05 14:45 | dom | DECISION D-16 | Aucun upload OwnCloud maintenant. Dom teste l'application sous Windows. Inno Setup à installer puis repackager avec installateur seulement après tests et GO explicite Dom.
|
2026-06-05 14:45 | dom | DECISION D-16 | Aucun upload OwnCloud maintenant. Dom teste l'application sous Windows. Inno Setup à installer puis repackager avec installateur seulement après tests et GO explicite Dom.
|
||||||
2026-06-05 17:55 | dom | DECISION D-17 | Post-bêta v11.5 à préparer en parallèle : GUI v6, D-13 complet, plateforme licence. Claude doit orchestrer des agents séparés, sans perturber le pack bêta v11 avant GO Windows.
|
2026-06-05 17:55 | dom | DECISION D-17 | Post-bêta v11.5 à préparer en parallèle : GUI v6, D-13 complet, plateforme licence. Claude doit orchestrer des agents séparés, sans perturber le pack bêta v11 avant GO Windows.
|
||||||
|
2026-06-05 18:05 | dom | ECRIT for-qwen | Qwen reçoit rôle revue transverse v11.5 en lecture seule : T-P découpage/conflits, T-Q matrice d'acceptation, T-R registre risques. Aucun code ni packaging avant GO bêta Windows.
|
||||||
2026-06-05 17:55 | dom | DECISION D-17 | Cap v11.5 : préparer en parallèle GUI v6 + D-13 complet + plateforme licence APRÈS GO bêta. Gel bêta : plans/inventaires/découpage seulement tant que tests Windows Dom pas finis. Demande plan parallèle à Claude.
|
2026-06-05 17:55 | dom | DECISION D-17 | Cap v11.5 : préparer en parallèle GUI v6 + D-13 complet + plateforme licence APRÈS GO bêta. Gel bêta : plans/inventaires/découpage seulement tant que tests Windows Dom pas finis. Demande plan parallèle à Claude.
|
||||||
2026-06-05 18:10 | claude | DEPOSE plan v11.5 | for-dom/2026-06-05_claude_plan-v11-5-parallele.md : 4 agents (A GUI v6 / B D-13 / C licence / D intégration), frontières fichiers, ordre merge, tests. ALERTE : WIP backup GUI v6 (b8c9c41) uniquement sur disque Windows → recommande push Gitea urgent. Gel bêta respecté (planif pure).
|
2026-06-05 18:10 | claude | DEPOSE plan v11.5 | for-dom/2026-06-05_claude_plan-v11-5-parallele.md : 4 agents (A GUI v6 / B D-13 / C licence / D intégration), frontières fichiers, ordre merge, tests. ALERTE : WIP backup GUI v6 (b8c9c41) uniquement sur disque Windows → recommande push Gitea urgent. Gel bêta respecté (planif pure).
|
||||||
|
2026-06-05 18:14 | claude | INSTALLATEUR v11 OK | Inno Setup déjà présent (user). build_windows_installer_only.ps1 → release\Anonymisation-Setup.exe 695.8 Mo, EXIT_CODE=0, non signé. SHA256=12D9D6A8...663DC. Manifeste SHA256SUMS.txt MAJ (EXE+ZIP+Setup). Rapport → for-dom/2026-06-05_claude_package-beta-installateur-maj.md. OwnCloud toujours en attente du canal (D-16).
|
||||||
|
2026-06-05 18:18 | claude | 4 AGENTS v11.5 LIVRÉS | Sous-plans A(GUI v6)/B(D-13)/C(licence)/D(intégration) déposés for-dom. CORRECTION Agent A : le WIP backup b8c9c41 n'est PAS une GUI v6 mais l'ancêtre tkinter de v5.5 (v5.5 en avance de 24 lignes). Vraie base v6 = mockup HTML + logique v5.5. customtkinter à ajouter aux requirements. Qwen a livré en // T-P/T-Q/T-R (revue transverse, via Codex) — recoupe Agent D.
|
||||||
|
|||||||
Reference in New Issue
Block a user