Cinquante-neuf chantiers clos entre le 5 et le 13 août 2026. Les lots F et J sont clos, et il ne reste aucun bug ouvert.
+ Soixante chantiers clos entre le 5 et le 13 août 2026. Les lots F et J sont clos, et il ne reste aucun bug ouvert.
+
+ Purge du journal d'audit, mention dans visitapp-web, et _toLangCode dédoublonné
+ ⚠️ La purge d'AuditLog avait été oubliée à la première passe du lot J, alors que le plan l'y assignait explicitement. AuditLog porte un UserId, et les valeurs avant/après d'une modification de User contiennent e-mail, prénom et nom — conservés sans limite, quand VisitEvent purge à 13 mois et VisitorQuestion à 90 jours. 12 mois, uniforme ; pas 13, les 13 mois des stats servent à comparer une saison à la précédente et les recopier serait du mimétisme — un test fige l'écart. Inerte tant qu'Audit:RetentionDays n'est pas défini : pas de pg_dump, pas de suppression.
+ ⚠️ ExecuteDeleteAsync n'est pas traduisible par le provider InMemory : la suppression se teste contre un vrai Postgres. L'écrire en chargeant les lignes puis RemoveRange l'aurait rendue testable en mémoire au prix de charger un an de journal — dégrader le code de production pour satisfaire un provider de test.
+ Mention visiteurs dans visitapp-web, panneau dépliable dans l'en-tête de l'assistant, même texte que l'app mobile. _toLangCode était copié trois fois et la décision « FR/NL/EN/DE seulement » n'était écrite que dans une copie : les deux autres ressemblaient à un oubli qu'on aurait « corrigé » en ajoutant des langues, produisant un support partiel silencieux. Une seule copie désormais, une seule décision.
+
+
Lot J — le volet codable est livré (thèmes, interrupteur, mention)
Table d'agrégats (instance, mois, thème, compteur) : c'est elle qui tient la promesse du §8.4 des CGU. Sans elle le regroupement vit dans la ligne VisitorQuestion, donc la purge du 90e jour l'emporte avec la question et le client perd tout au 91e. Elle ne porte que des compteurs — aucune donnée personnelle, ce qui est précisément ce qui l'autorise à survivre. ⚠️ Insights lit désormais les thèmes dans cette table, pas dans les questions de la fenêtre.
diff --git a/v1-plan.md b/v1-plan.md
index db0e5ae..11c86bd 100644
--- a/v1-plan.md
+++ b/v1-plan.md
@@ -365,7 +365,7 @@ Ordre du §2 de STATUS.md, corrigé par **L12**.
Les CGU §8 ont été réécrites le 2026-08-11 (`cgu-myinfomate.md`) et le texte d'information visiteurs existe (`mention-information-visiteurs.md`). Ce qui reste :
-✅ **Volet codable du lot J livré le 2026-08-13** — J1, J2, J3 et J4 (côté `mymuseum-visitapp`). `dotnet test` **226**, APK `dev` ✅, `flutter build web` ✅. Migration unique `LotJ_ThemeAggregatesAndCollectionSwitch` : une colonne + une table, rien d'autre — vérifié dans le fichier généré.
+✅ **Lot J : tout le codable est livré le 2026-08-13** — J1, J2, J3, J4 (les deux apps visiteur) et la purge du journal d'audit. `dotnet test` **229**, APK `dev` ✅, `flutter build web` ✅, `npm run build` ✅. Migration unique `LotJ_ThemeAggregatesAndCollectionSwitch` : une colonne + une table, rien d'autre — vérifié dans le fichier généré. **Ce qui reste au lot J ne s'écrit pas en code** : relecture juridique du §8, conditions réelles de Google, DPA séparé, et les traductions relues de la mention.
⚠️ **Les thèmes sont une liste fixe de 8, pas des thèmes découverts par l'IA** — décidé le 2026-08-13. Des libellés régénérés à chaque passage produiraient « Horaires » en janvier et « Questions d'horaires » en février : deux lignes d'agrégat distinctes, et une courbe qui ne veut rien dire. Or la table existe précisément pour porter cet historique. **Ajouter un thème reste possible, en renommer un coupe l'historique en deux.**
@@ -380,7 +380,8 @@ Les CGU §8 ont été réécrites le 2026-08-11 (`cgu-myinfomate.md`) et le text
| ~~J1~~ ✅ | **Table d'agrégats `QuestionThemeMonthly`** | `(InstanceId, mois, thème, compteur)`, index sur `(InstanceId, Month)`. **Ne porte que des compteurs** — ni texte, ni session, ni langue : aucune donnée personnelle, donc rien qui justifierait de la purger, ce qui est précisément ce qui l'autorise à survivre. ⚠️ **`Insights` lit désormais `topics` et `themes` dans cette table, pas dans les questions de la fenêtre** — les lire dans les questions faisait disparaître l'historique au 91ᵉ jour, sans erreur ni trace |
| ~~J2~~ ✅ | **`QuestionThemingService`**, quotidien à 2 h | Classement par lots de 25 en un appel — un appel par question multiplierait le coût par 25 pour le même travail. **6 tests** |
| ~~J3~~ ✅ | **`Instance.IsVisitorQuestionCollectionEnabled`**, défaut `true` | Garde dans `Chat`, exposé au DTO, et **interrupteur dans l'écran Guide IA** — sans UI le client ne pourrait pas exercer le refus dont il est responsable. Le libellé dit ce que couper coûte (l'onglet cesse de se remplir) et ce que couper ne fait pas (rien n'est effacé, les questions déjà là vivent jusqu'à leur purge) |
-| 🔨 J4 | **Mention aux visiteurs** | ✅ **`mymuseum-visitapp`** : icône dans l'en-tête de l'assistant → `VisitorPrivacyNotice`, texte repris **mot pour mot** de `mention-information-visiteurs.md`.
⚠️ **Trois langues seulement (FR/NL/EN), repli sur l'anglais — c'est un choix.** L'app en porte dix, mais traduire une mention de protection des données sans relecture humaine serait pire que la servir en anglais : une nuance perdue sur « nous n'enregistrons pas votre adresse IP » n'est pas une coquille d'interface. **À demander avec la relecture juridique (J5).**
⛔ **Reste `visitapp-web`** — même mention, autre repo |
+| ~~J4~~ ✅ | **Mention aux visiteurs** | Livrée dans **`mymuseum-visitapp`** (`VisitorPrivacyNotice`) **et `visitapp-web`** (panneau dépliable dans l'en-tête de l'assistant), texte repris **mot pour mot** de `mention-information-visiteurs.md` dans les deux.
⚠️ **Trois langues seulement (FR/NL/EN), repli sur l'anglais — c'est un choix.** Les apps en portent dix et six, mais traduire une mention de protection des données sans relecture humaine serait pire que la servir en anglais : une nuance perdue sur « nous n'enregistrons pas votre adresse IP » n'est pas une coquille d'interface. **À demander avec la relecture juridique (J5).**
⚠️ **Trois copies du même texte** (le document, le Dart, le TSX) : une correction se fait dans le document d'abord, les deux autres suivent. C'est écrit dans les deux fichiers de code |
+| ~~J8~~ ✅ | **Purge du journal d'audit** — ajouté le 2026-08-13 | ⚠️ **Poste oublié à la première passe du lot J** : le plan l'y assignait explicitement (« à traiter au lot J, pas au lot F »), il n'avait pas été fait. `AuditLog` porte un `UserId`, et les valeurs avant/après d'une modification de `User` contiennent e-mail, prénom et nom — **conservés sans limite** jusqu'ici, quand `VisitEvent` purge à 13 mois et `VisitorQuestion` à 90 jours.
**12 mois, uniforme**, via `AuditLogPurgeService` calqué sur `VisitEventPurgeService`. Pas 13 : les 13 mois des stats servent à comparer une saison à la précédente, les recopier ici serait du mimétisme — **un test fige cet écart voulu**.
⚠️ **Inerte tant qu'`Audit:RetentionDays` n'est pas défini**, même verrou que les `VisitEvent` : pas de pg_dump, pas de suppression définitive.
⚠️ **`ExecuteDeleteAsync` n'est pas traduisible par le provider InMemory** — la suppression se teste contre un vrai Postgres. L'écrire en chargeant les lignes puis `RemoveRange` l'aurait rendue testable en mémoire au prix de charger un an de journal : dégrader le code de production pour satisfaire un provider de test |
| J5 | **Faire relire le §8 des CGU** | Document contractuel, rédigé côté produit et non validé juridiquement. Priorité au §8.6 (sous-traitants) et au §8.5 (transfert hors UE) |
| J6 | **Vérifier les conditions réelles de Google** | Le §8.5 dit « peut impliquer un transfert hors UE » — prudent mais vague. Savoir si l'API Gemini utilisée offre une résidence européenne, et si un DPA est signé. La réponse réécrit le §8.5 |
| J7 | **Décider si un DPA séparé est requis** | L'article 28 exige un acte écrit. Le §8 en tient partiellement lieu ; une commune ou un musée subsidié en demandera un en annexe |