Lot F livré côté serveur, manager-app et landing — et six points de STATUS qui affirmaient du faux
Le volet manager-service du lot F est refermé, plus le bouton du portail, le diviseur de questions et la FAQ de la landing. Ce que la vérification dans le code a corrigé dans les docs : - la ligne « À faire » du §1 de STATUS était périmée sur six points. Le rate limiting y figurait comme à faire alors qu'il est livré (politique, UseRateLimiter et les deux endpoints décorés), tout comme l'écran d'audit log, la gestion des users et ParcoursIds. L'unification QuestionType n'est pas « à faire » mais écartée. - l'endpoint de mise à jour d'ApplicationInstance n'a jamais manqué : le CRUD est complet. Ce qui manque est l'écran SuperAdmin, déjà parqué en V2. - les quotas seed 5M/20M étaient déjà alignés. Kanban : la carte du portail est close, deux entrées entrent dans Fait récemment. Compteurs re-mesurés — Planifié 25, Fait récemment 51 — colonnes et bandeau vérifiés l'un contre l'autre, jamais par delta. Ce commit emporte aussi les 3 cartes V2 stagées par une autre session (nettoyage downloadConfiguration, swagger périmés, colonne path écrasée) : elles vivent dans le même fichier kanban.html. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
fdd0213757
commit
6c455e4d7c
13
STATUS.md
13
STATUS.md
@ -14,7 +14,8 @@ Résumé de la table de suivi de ce fichier :
|
|||||||
- **Fait** : SectionEvent, Escape/Puzzle games, GuidedPath + progression carte, push, stats tracking, sync agenda/météo, traduction IA, quotas (IA/stockage/stats) par plan, audit log backend fiabilisé, refacto SectionParcours/GuidedPath/SectionGame (backend, vérifié 2026-07-15).
|
- **Fait** : SectionEvent, Escape/Puzzle games, GuidedPath + progression carte, push, stats tracking, sync agenda/météo, traduction IA, quotas (IA/stockage/stats) par plan, audit log backend fiabilisé, refacto SectionParcours/GuidedPath/SectionGame (backend, vérifié 2026-07-15).
|
||||||
- **Partiel** : Beacons/geofencing (manager-app UI ⚠️), AI Assistant (manager-app ⚠️), **mode offline — diagnostic posé le 2026-08-07, bien pire qu'un « sélectif » : le pipeline de collecte des ressources est commenté des deux côtés, une visite téléchargée n'embarque ni images d'articles ni audios → [v2/offline-visit-plan.md](v2/offline-visit-plan.md)**, repasse UI globale, refacto SectionParcours côté manager-app/visitapp-web/mymuseum-visitapp (UI présente mais sous-comportements non revérifiés en détail — voir section "Refactoring majeur" de todo-features.md).
|
- **Partiel** : Beacons/geofencing (manager-app UI ⚠️), AI Assistant (manager-app ⚠️), **mode offline — diagnostic posé le 2026-08-07, bien pire qu'un « sélectif » : le pipeline de collecte des ressources est commenté des deux côtés, une visite téléchargée n'embarque ni images d'articles ni audios → [v2/offline-visit-plan.md](v2/offline-visit-plan.md)**, repasse UI globale, refacto SectionParcours côté manager-app/visitapp-web/mymuseum-visitapp (UI présente mais sous-comportements non revérifiés en détail — voir section "Refactoring majeur" de todo-features.md).
|
||||||
- **🧪 Implémenté mais jamais testé (2026-08-05)** : **onboarding self-service complet** — inscription sur myinfomate-landing (`/[lang]/signup`), création auto de l'instance + du premier user, essai 14j, Stripe (customer/Checkout/webhook/Tax), 9 emails Resend (domaine vérifié, envoi réel validé), job Hangfire du cycle d'essai, watermark « Aperçu » visitapp-web, écran Abonnement manager-app, mot de passe oublié + invitation user, plafond IA d'essai. Les 4 repos compilent, migrations appliquées en base locale, **aucun parcours exécuté** → checklist de test dans `todo-features.md` (section "🧪 Checklist de test end-to-end"). Reste à faire côté config : alias mail `onboarding@myinfomate.be`, et `whsec_` de la Stripe CLI pour tester le webhook en local.
|
- **🧪 Implémenté mais jamais testé (2026-08-05)** : **onboarding self-service complet** — inscription sur myinfomate-landing (`/[lang]/signup`), création auto de l'instance + du premier user, essai 14j, Stripe (customer/Checkout/webhook/Tax), 9 emails Resend (domaine vérifié, envoi réel validé), job Hangfire du cycle d'essai, watermark « Aperçu » visitapp-web, écran Abonnement manager-app, mot de passe oublié + invitation user, plafond IA d'essai. Les 4 repos compilent, migrations appliquées en base locale, **aucun parcours exécuté** → checklist de test dans `todo-features.md` (section "🧪 Checklist de test end-to-end"). Reste à faire côté config : alias mail `onboarding@myinfomate.be`, et `whsec_` de la Stripe CLI pour tester le webhook en local.
|
||||||
- **À faire** : écran audit log manager-app (backend ✅, front ❌ — ajouté le 2026-07-15), rate limiting API Keys, gestion users par instance admin, unification QuestionType (TextLibre/Digicode/ExpectedAnswer), nettoyage SectionEvent.ParcoursIds, Stripe Customer Portal, dashboard super-admin V2, facturation V2.
|
- **À faire** : dashboard super-admin V2, facturation V2. ✅ **Le Stripe Customer Portal est complet le 2026-08-13** (backend + bouton manager-app).
|
||||||
|
- ⚠️ **Cette ligne était périmée sur six points, revérifiés dans le code le 2026-08-13.** Sont **faits** : écran audit log manager-app (front livré le 12/08), **rate limiting API Keys** (livré, pas seulement décidé — politique `ai` dans `Startup.cs:279`, `UseRateLimiter()` en `:344`, les deux endpoints décorés), gestion des users par instance (plafond serveur + compteur UI, 12/08), nettoyage `SectionEvent.ParcoursIds` (11/08), et le **backend** du Customer Portal (13/08). L'**unification `QuestionType`** n'est pas « à faire » mais **écartée** : le trio TextLibre/Digicode/ExpectedAnswer n'a jamais existé dans le code, l'enum a trois valeurs et le digicode est un comportement dérivé, pas un type — reste une propreté front, elle aussi livrée (alias nommés, 11/08).
|
||||||
- **📦 Reporté en V2 (décidé le 2026-08-07)** : **SectionForm**, **ressource 360°**, **AR image tracking (Mind AR)**. Les trois sont spécifiés en détail dans [todo-features.md](todo-features.md) mais sortent du périmètre V1 — aucun n'est un prérequis de la migration Postgres ni de la mise en prod. À reprendre tels quels quand la V1 sera stabilisée.
|
- **📦 Reporté en V2 (décidé le 2026-08-07)** : **SectionForm**, **ressource 360°**, **AR image tracking (Mind AR)**. Les trois sont spécifiés en détail dans [todo-features.md](todo-features.md) mais sortent du périmètre V1 — aucun n'est un prérequis de la migration Postgres ni de la mise en prod. À reprendre tels quels quand la V1 sera stabilisée.
|
||||||
|
|
||||||
**Prochaine chose à faire si tu as 1h** : jouer le **§19.13 cas E** du plan de test (chasse au trésor / carnaval) de bout en bout, depuis la création dans manager-app. C'est le scénario le plus complet — il valide SectionMap + SectionParcours + géodéclenchement + questions + mode jeu d'un coup.
|
**Prochaine chose à faire si tu as 1h** : jouer le **§19.13 cas E** du plan de test (chasse au trésor / carnaval) de bout en bout, depuis la création dans manager-app. C'est le scénario le plus complet — il valide SectionMap + SectionParcours + géodéclenchement + questions + mode jeu d'un coup.
|
||||||
@ -273,7 +274,11 @@ Ordre à respecter, sous peine de travailler deux fois :
|
|||||||
|
|
||||||
### Restes non chiffrés, à caser en cours de route
|
### Restes non chiffrés, à caser en cours de route
|
||||||
|
|
||||||
Ratio jetons → questions (`/1000`) à caler sur du réel · endpoint de mise à jour d'`ApplicationInstance` pour rendre les canaux activables depuis l'écran · quotas seed 5M/20M tokens à ajuster · ~~`features.reqPerMonth` de la landing~~ ✅ corrigé, la clé n'existe plus dans `myinfomate-landing/src` (vérifié le 2026-08-11).
|
~~Ratio jetons → questions (`/1000`) à caler sur du réel~~ ✅ **des deux côtés le 2026-08-13.** Serveur : `InstanceQuotaDTO.aiTokensPerQuestion`, mesuré sur les `VisitorQuestion.TokensUsed` de l'instance, avec un seuil de 20 questions avant de faire foi ; en dessous, repli sur l'hypothèse de la grille tarifaire. Front : `guide_ia_screen` lit ce diviseur au lieu du `/1000` codé en dur, et **masque la ligne « ~N questions »** si l'appel échoue. ⚠️ **Le `/1000` était faux d'un facteur 10** — la grille vend Premium à 20 M de jetons pour ~2 000 questions, soit **10 000 par question** — et c'est un chiffre montré au client : le crédit restant affiché était dix fois trop généreux.
|
||||||
|
|
||||||
|
⛔ ~~Endpoint de mise à jour d'`ApplicationInstance`~~ **n'a jamais manqué** (vérifié le 2026-08-13) : `ApplicationInstanceController` porte un CRUD complet (`POST :75`, `PUT :115`) et `InstanceController.Updateinstance` écrit déjà `IsMobile`/`IsTablet`/`IsWeb` (`:209-211`). Activer un canal est faisable par l'API depuis le début ; ce qui manque est l'**écran** SuperAdmin, parqué en V2 (voir §« Add-on IA »). ⛔ ~~Quotas seed 5M/20M~~ **déjà ajustés** par `AlignSubscriptionPlansWithPricing` : Essentiel 0, Pro 0, Premium 20 M, Enterprise `long.MaxValue`. Le « 5M » datait d'avant l'alignement.
|
||||||
|
|
||||||
|
~~`features.reqPerMonth` de la landing~~ ✅ corrigé, la clé n'existe plus dans `myinfomate-landing/src` (vérifié le 2026-08-11).
|
||||||
|
|
||||||
### Hors périmètre V1, assumé
|
### Hors périmètre V1, assumé
|
||||||
|
|
||||||
@ -430,6 +435,10 @@ Remplacé par `GetReferencedResourceIds()`, déjà implémentée sur les 13 sous
|
|||||||
|
|
||||||
**Lot F — le volet `manager-service` est livré le 2026-08-12 : les quatre chantiers de dette backend.** `dotnet test` **163 → 200**, aucun test sauté. Quatre commits, `manager-service` seul.
|
**Lot F — le volet `manager-service` est livré le 2026-08-12 : les quatre chantiers de dette backend.** `dotnet test` **163 → 200**, aucun test sauté. Quatre commits, `manager-service` seul.
|
||||||
|
|
||||||
|
✅ **Volet `manager-service` refermé le 2026-08-13** — Stripe Customer Portal (backend) et ratio jetons → questions calé sur du réel. **6 tests**, `dotnet test` **211 : 197 passés, 14 sautés** (Testcontainers sans démon Docker), 0 échec. **Trois des cinq points restants étaient déjà faits** : rate limiting, endpoint `ApplicationInstance`, quotas seed — détail au §« Restes non chiffrés » ci-dessus et dans [v1-plan.md lot F](v1-plan.md).
|
||||||
|
|
||||||
|
> ⚠️ **Ce qui reste du lot F n'est plus dans `manager-service`** : le bouton du portail et le diviseur `aiTokensPerQuestion` dans `manager-app` (petit), et surtout les trois chantiers `mymuseum-visitapp` — déclenchement proactif (+ M3), miroir de la conversation vocale, émission des événements vocaux. Plus l'aperçu de conversation (**L9**), indépendant.
|
||||||
|
|
||||||
**1. Le journal d'audit voyait tout sauf le contenu.** `AuditedTypes.Contains(entry.Entity.GetType())` exigeait l'égalité **exacte** de type, or `Section` est **abstraite** : le type runtime est toujours `SectionMap`, `SectionQuiz`… Aucune des 13 sortes de section n'était journalisée — soit précisément ce que l'écran du 12/08 devait tracer. `Resource`, `Configuration`, `Device`, `User` et `Instance` passaient, eux : ils sont concrets, et **c'est ce qui rendait le trou invisible**. Remplacé par une remontée à la classe de base auditée (`IsInstanceOfType`).
|
**1. Le journal d'audit voyait tout sauf le contenu.** `AuditedTypes.Contains(entry.Entity.GetType())` exigeait l'égalité **exacte** de type, or `Section` est **abstraite** : le type runtime est toujours `SectionMap`, `SectionQuiz`… Aucune des 13 sortes de section n'était journalisée — soit précisément ce que l'écran du 12/08 devait tracer. `Resource`, `Configuration`, `Device`, `User` et `Instance` passaient, eux : ils sont concrets, et **c'est ce qui rendait le trou invisible**. Remplacé par une remontée à la classe de base auditée (`IsInstanceOfType`).
|
||||||
|
|
||||||
> **Tranché : normalisation côté serveur.** `EntityType` porte `Section`, pas `SectionMap`. Trois raisons : le filtre « Section » **déjà présent** dans l'écran se met à rendre des lignes sans toucher `manager-app`, donc sans coordonner deux repos ; élargir le filtre front aurait coûté 13 entrées de liste et 39 clés i18n **et** laissé tout futur sous-type sortir du filtre en silence — la classe de panne exacte qu'on ferme ici ; et le sous-type concret **n'est pas perdu**, le discriminateur TPH est une propriété du modèle, donc sérialisée dans `NewValues` (`"Discriminator": "Article"`). Un test le vérifie — ce n'était pas une supposition.
|
> **Tranché : normalisation côté serveur.** `EntityType` porte `Section`, pas `SectionMap`. Trois raisons : le filtre « Section » **déjà présent** dans l'écran se met à rendre des lignes sans toucher `manager-app`, donc sans coordonner deux repos ; élargir le filtre front aurait coûté 13 entrées de liste et 39 clés i18n **et** laissé tout futur sous-type sortir du filtre en silence — la classe de panne exacte qu'on ferme ici ; et le sous-type concret **n'est pas perdu**, le discriminateur TPH est une propriété du modèle, donc sérialisée dans `NewValues` (`"Discriminator": "Article"`). Un test le vérifie — ce n'était pas une supposition.
|
||||||
|
|||||||
62
kanban.html
62
kanban.html
@ -458,9 +458,9 @@
|
|||||||
<div class="stat"><span class="n n-info">1</span><span class="k">Migration v3</span></div>
|
<div class="stat"><span class="n n-info">1</span><span class="k">Migration v3</span></div>
|
||||||
<div class="stat"><span class="n n-warn">2</span><span class="k">Bugs ouverts</span></div>
|
<div class="stat"><span class="n n-warn">2</span><span class="k">Bugs ouverts</span></div>
|
||||||
<div class="stat"><span class="n">6</span><span class="k">À tester</span></div>
|
<div class="stat"><span class="n">6</span><span class="k">À tester</span></div>
|
||||||
<div class="stat"><span class="n">23</span><span class="k">Planifié</span></div>
|
<div class="stat"><span class="n">25</span><span class="k">Planifié</span></div>
|
||||||
<div class="stat"><span class="n n-gate">8</span><span class="k">Bascule prod</span></div>
|
<div class="stat"><span class="n n-gate">8</span><span class="k">Bascule prod</span></div>
|
||||||
<div class="stat"><span class="n n-good">48</span><span class="k">Fait récemment</span></div>
|
<div class="stat"><span class="n n-good">51</span><span class="k">Fait récemment</span></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="filters" role="group" aria-label="Filtrer par domaine">
|
<div class="filters" role="group" aria-label="Filtrer par domaine">
|
||||||
@ -596,11 +596,36 @@
|
|||||||
|
|
||||||
<!-- PLANIFIÉ -->
|
<!-- PLANIFIÉ -->
|
||||||
<section class="col" style="--stripe: var(--ink-3)">
|
<section class="col" style="--stripe: var(--ink-3)">
|
||||||
<div class="col-head"><h2>Planifié</h2><span class="count">23</span></div>
|
<div class="col-head"><h2>Planifié</h2><span class="count">25</span></div>
|
||||||
|
|
||||||
|
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
|
|
||||||
|
<article class="card" data-area="visitapp" data-horizon="v2">
|
||||||
|
<div class="card-meta"><span class="tag">visitapp</span><span class="tag">code mort</span><span class="flag f-warn">V2</span></div>
|
||||||
|
<h3>Nettoyer downloadConfiguration.dart</h3>
|
||||||
|
<p>Trouvé en livrant D4 le 12/08, laissé volontairement hors périmètre du lot D. Deux blocs morts dans le même fichier :</p>
|
||||||
|
<p><strong>1. ~130 lignes de classe commentée</strong> — <code>/*class DownloadConfiguration {</code> ligne 328 jusqu'au <code>*/</code> ligne 461. C'est <strong>ce bloc qui a fait croire au plan V1 que « les deux chemins de téléchargement divergent »</strong> : la ligne citée comme preuve vivait dedans. Un doublon commenté qui ressemble à du code actif coûte plus cher que pas de code du tout — il a produit un diagnostic faux.</p>
|
||||||
|
<p><strong>2. <code>cleanLocalResources</code>, appelée nulle part</strong> — et non réactivable telle quelle : la table locale <code>resources</code> n'a pas de colonne <code>configurationId</code>, elle est globale à toutes les visites, et le paramètre <code>configuration</code> de la fonction n'est jamais utilisé. Deux issues : la <strong>supprimer</strong>, ou lui donner un périmètre en ajoutant la colonne. ⚠️ Conséquence à assumer si on supprime : <strong>les lignes DB des ressources retirées d'une visite ne sont purgées par personne</strong>. Impact réel faible — rien ne les lit pour le rendu, <code>CachedCustomResource</code> retrouve le fichier en listant le répertoire — mais la table croît sans borne.</p>
|
||||||
|
<span class="src">v1-plan.md lot D — D4 · STATUS.md §K5</span>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card" data-area="backend manager visitapp" data-horizon="v2">
|
||||||
|
<div class="card-meta"><span class="tag">3 repos</span><span class="flag f-warn">Contredit le code</span></div>
|
||||||
|
<h3>Les swagger.yaml embarqués sont périmés</h3>
|
||||||
|
<p>Relevé en livrant D2 le 12/08. Les trois <code>lib/api/swagger.yaml</code> (manager-app, mymuseum-visitapp, tablet-app) décrivent un <code>ResourceDTO</code> qui n'a <strong>ni <code>sizeBytes</code></strong> (ajouté à l'époque de C1/C3) <strong>ni <code>dateUpdate</code></strong> (D2).</p>
|
||||||
|
<p>Ce sont des artefacts de génération, et <strong>le client s'édite à la main</strong> : ils ne sont plus la source de vérité. Le risque n'est pas fonctionnel — rien ne les lit à l'exécution — mais <strong>ils contredisent le code en silence</strong>, et c'est exactement ce qui a coûté du temps sur le « fallback Voice → Mobile » du lot F : une doc affirmative et fausse. Deux issues : les <strong>régénérer une fois</strong> et les tenir, ou les <strong>supprimer</strong> et assumer que le Swagger en ligne est la seule référence.</p>
|
||||||
|
<span class="src">STATUS.md §K5 — relevé D2</span>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card" data-area="visitapp" data-horizon="v2">
|
||||||
|
<div class="card-meta"><span class="tag">visitapp</span><span class="tag">piège latent</span><span class="flag f-warn">V2</span></div>
|
||||||
|
<h3>La colonne path de la table resources est écrasée à vide</h3>
|
||||||
|
<p>Trouvé en livrant D2 le 12/08. <code>DatabaseHelper.insert</code> fait un <strong>UPDATE de la ligne entière</strong> quand l'id existe déjà : la boucle qui enregistre la charge de ressources (héritée de D1) repasse derrière la boucle de téléchargement et réécrit <code>path</code> avec la valeur par défaut de <code>ResourceModel</code>, soit <code>""</code>.</p>
|
||||||
|
<p><strong>Sans effet aujourd'hui</strong> : personne ne lit cette colonne pour le rendu. Mais elle est <code>NOT NULL</code> et prétend porter le chemin local — <strong>le premier code qui s'y fiera trouvera une chaîne vide</strong>, sans erreur. Même famille que le <code>dateUpdate</code> effacé par la même boucle, corrigé en D2 parce que lui, il est lu. Deux issues : renseigner <code>path</code> dans les deux boucles, ou retirer la colonne et assumer que le répertoire fait foi.</p>
|
||||||
|
<span class="src">v1-plan.md lot D — D2</span>
|
||||||
|
</article>
|
||||||
|
|
||||||
<article class="card" data-area="commercial" data-horizon="v1">
|
<article class="card" data-area="commercial" data-horizon="v1">
|
||||||
<div class="card-meta"><span class="tag">après prod</span><span class="tag">relationnel</span></div>
|
<div class="card-meta"><span class="tag">après prod</span><span class="tag">relationnel</span></div>
|
||||||
<h3>Reprendre contact avec Louise Smets — Musée d'Ixelles</h3>
|
<h3>Reprendre contact avec Louise Smets — Musée d'Ixelles</h3>
|
||||||
@ -665,15 +690,6 @@
|
|||||||
<span class="src">v1-plan.md lot F — miroir vocal</span>
|
<span class="src">v1-plan.md lot F — miroir vocal</span>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="card" data-area="backend manager" data-horizon="v1">
|
|
||||||
<div class="card-meta"><span class="tag">manager-service</span><span class="tag">manager-app</span><span class="flag f-warn">Confirmé V1 le 12/08</span></div>
|
|
||||||
<h3>Stripe Customer Portal — changer sa carte</h3>
|
|
||||||
<p><strong>État réel, mesuré le 12/08 — moins grave que le plan ne le disait, mais bien cassé.</strong> L'e-mail d'échec de paiement pointe vers <code>{managerAppUrl}/billing</code> (<code>StripeWebhookController:124</code>), donc vers un écran à toi, pas vers une URL Stripe fantôme. Et cet écran <strong>existe</strong> (<code>Screens/Billing/subscription_screen.dart</code>).</p>
|
|
||||||
<p>⚠️ <strong>Mais il ne sait faire qu'une chose : lancer un Checkout de souscription.</strong> Scénario réel : la carte d'un client expire, il reçoit l'e-mail, clique, et arrive sur un écran qui lui propose de souscrire l'abonnement qu'il a déjà. Il t'appelle. À 4 clients c'est gérable, en self-service non.</p>
|
|
||||||
<p><code>StripeService</code> sait créer un client et une Checkout Session, mais pas de session de portail. Endpoint <code>Stripe.BillingPortal</code> + bouton dans l'écran existant — Stripe héberge la page, il n'y a <strong>aucune UI de paiement à écrire</strong>. ~1 jour.</p>
|
|
||||||
<span class="src">v1-plan.md lot F · STATUS.md §1 « À faire »</span>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="card" data-area="manager" data-horizon="v1">
|
<article class="card" data-area="manager" data-horizon="v1">
|
||||||
<div class="card-meta"><span class="tag">manager-app</span><span class="flag f-warn">Jamais ouvert</span></div>
|
<div class="card-meta"><span class="tag">manager-app</span><span class="flag f-warn">Jamais ouvert</span></div>
|
||||||
<h3>Écrans du lot design — vérification à l'œil</h3>
|
<h3>Écrans du lot design — vérification à l'œil</h3>
|
||||||
@ -872,9 +888,27 @@
|
|||||||
|
|
||||||
<section class="done">
|
<section class="done">
|
||||||
<h2>Fait récemment</h2>
|
<h2>Fait récemment</h2>
|
||||||
<p>Quarante-huit chantiers clos entre le 5 et le 12 août 2026.</p>
|
<p>Cinquante-et-un chantiers clos entre le 5 et le 13 août 2026.</p>
|
||||||
<div class="done-grid">
|
<div class="done-grid">
|
||||||
|
|
||||||
|
<div class="done-item">
|
||||||
|
<strong>FAQ de la landing — le plan à 179 € s'appelait encore « Bundle »</strong>
|
||||||
|
<span>41 occurrences en 4 langues (FR, EN, NL, DE) dans <code>src/data/segments.ts</code>, alors que les cartes de tarifs disent « Premium » : un prospect qui lisait la grille puis la FAQ voyait <strong>deux noms pour la même offre</strong>. Les formes composées des langues germaniques suivent (<code>Bundle-abonnementen</code> → <code>Premium-abonnementen</code>, <code>Bundle-Pläne</code> → <code>Premium-Pläne</code>). Vérifié : plus aucun « Bundle » dans <code>myinfomate-landing/src</code>, et les 41 occurrences étaient toutes de la prose — aucun identifiant touché. <code>npm run build</code> vert.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="done-item">
|
||||||
|
<strong>Lot F — le bouton du portail Stripe et le diviseur de questions (manager-app)</strong>
|
||||||
|
<span><strong>Portail de facturation</strong> : le bouton manquait, et l'écran <strong>n'affichait rien du tout hors essai</strong> — <code>if (isTrialActive)</code> enveloppait le seul bouton, donc un client payant arrivait sur un écran sans action. C'est désormais Checkout <strong>pendant</strong> l'essai, portail <strong>après</strong> ; les deux ne coexistent jamais. ⚠️ <strong>Le 409 a son propre message</strong> : les 4 instances de la bascule viennent de Mongo et n'ont pas de <code>StripeCustomerId</code> — « réessayez plus tard » les ferait attendre quelque chose qui n'arrivera jamais. 4 clés i18n FR/EN/NL.</span>
|
||||||
|
<span><strong>Diviseur de questions</strong> : <code>guide_ia_screen</code> lit <code>aiTokensPerQuestion</code> du serveur au lieu du <code>/1000</code> codé en dur, par appel <code>http</code> direct — le motif déjà établi dans cet écran pour <code>knowledge</code> et <code>insights</code>. ⚠️ <strong>Si l'appel échoue, la ligne « ~N questions » disparaît</strong> plutôt que d'afficher un repli : sur une jauge qui sert à décider d'un achat, pas de chiffre vaut mieux qu'un chiffre faux — ce que le <code>/1000</code> faisait depuis le début, à un facteur 10 près. <code>flutter analyze lib</code> sans erreur, <code>flutter build web</code> ✅.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="done-item">
|
||||||
|
<strong>Lot F backend — portail de facturation Stripe et coût réel d'une question</strong>
|
||||||
|
<span><strong>Customer Portal</strong> : <code>StripeService.CreateBillingPortalSessionAsync</code> + <code>POST /api/onboarding/billing-portal</code>, calqué sur <code>checkout-session</code>. ⚠️ <strong>Une garde absente de l'énoncé</strong> : <code>StripeCustomerId</code> peut être nul. <code>CreateCustomerAsync</code> n'est appelée que par l'inscription self-service, donc <strong>les 4 instances de la bascule, venues de Mongo, n'ont jamais vu Stripe</strong> — sans la garde, elles partaient chercher le portail d'un client inexistant et récupéraient une erreur d'API Stripe illisible côté front. C'est un 409, distinct du 404 d'instance inconnue.</span>
|
||||||
|
<span><strong>Ratio jetons → questions</strong> : <code>InstanceQuotaDTO.aiTokensPerQuestion</code> est désormais mesuré sur les <code>VisitorQuestion.TokensUsed</code> de l'instance, avec un seuil de 20 questions avant de faire foi — une seule réponse citant un long article doublerait la moyenne, et le crédit restant afficherait moitié moins d'un rafraîchissement à l'autre. ⚠️ <strong>Le <code>/1000</code> de manager-app était faux d'un facteur 10, et c'est un chiffre montré au client</strong> : la grille vend Premium à 20 M de jetons pour ~2 000 questions, soit 10 000 par question. Le gestionnaire lisait un crédit dix fois trop généreux. Le repli serveur est calé sur 10 000. <strong>Reste à brancher le diviseur dans manager-app.</strong></span>
|
||||||
|
<span>⛔ <strong>Trois points du lot F étaient déjà faits et n'attendaient qu'une vérification</strong> : le rate limiting (livré, pas seulement décidé), l'endpoint de mise à jour d'<code>ApplicationInstance</code> (CRUD complet existant — ce qui manque est l'écran SuperAdmin, déjà parqué en V2) et les quotas seed (alignés par <code>AlignSubscriptionPlansWithPricing</code>). 6 tests ajoutés, <code>dotnet test</code> 197 passés / 14 sautés / 0 échec.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="done-item">
|
<div class="done-item">
|
||||||
<strong>C4 — compression des images à l'upload, 2560 px / JPEG q82</strong>
|
<strong>C4 — compression des images à l'upload, 2560 px / JPEG q82</strong>
|
||||||
<span><code>flutter build web</code> vert. Un seul helper appelé par les <strong>deux</strong> chemins d'upload de <code>resources_screen</code> — le motif exact qui a fait diverger <code>Create</code> et <code>Upload</code> côté serveur, sur <code>StoragePath</code> en C1 puis sur le pré-vol de quota en C3. ⚠️ <strong>La compression se fait avant <code>resourceCreate</code>, pas avant l'upload</strong> : le quota de C3 porte sur <code>sizeBytes</code> à la création, donc compresser après aurait fait décider le serveur sur la taille d'origine — et un quota qui compte 12× trop se remplit 12× trop vite.</span>
|
<span><code>flutter build web</code> vert. Un seul helper appelé par les <strong>deux</strong> chemins d'upload de <code>resources_screen</code> — le motif exact qui a fait diverger <code>Create</code> et <code>Upload</code> côté serveur, sur <code>StoragePath</code> en C1 puis sur le pré-vol de quota en C3. ⚠️ <strong>La compression se fait avant <code>resourceCreate</code>, pas avant l'upload</strong> : le quota de C3 porte sur <code>sizeBytes</code> à la création, donc compresser après aurait fait décider le serveur sur la taille d'origine — et un quota qui compte 12× trop se remplit 12× trop vite.</span>
|
||||||
@ -1187,7 +1221,7 @@
|
|||||||
|
|
||||||
// « Fait récemment » n'a pas d'horizon : c'est du passé, pas du reste-à-faire.
|
// « Fait récemment » n'a pas d'horizon : c'est du passé, pas du reste-à-faire.
|
||||||
if (done) { done.classList.toggle('hidden', horizon !== 'all'); }
|
if (done) { done.classList.toggle('hidden', horizon !== 'all'); }
|
||||||
if (stats[5]) { stats[5].classList.toggle('hidden', horizon !== 'all'); }
|
if (stats[6]) { stats[6].classList.toggle('hidden', horizon !== 'all'); }
|
||||||
if (emptyMsg) { emptyMsg.classList.toggle('hidden', total !== 0); }
|
if (emptyMsg) { emptyMsg.classList.toggle('hidden', total !== 0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
v1-plan.md
23
v1-plan.md
@ -174,6 +174,21 @@ Ne change pas : la popup de traduction (un niveau justifié, langues verticales,
|
|||||||
|
|
||||||
🔨 **Volet `manager-service` livré le 2026-08-12** — les quatre chantiers de dette backend sont fermés en une passe : journalisation des sections, plafond de 5 utilisateurs, `GetSummary` en SQL, tests du vector store. `dotnet test` **163 → 200**. Restent l'aperçu de conversation (L9), l'onglet « Vocal », les restes non chiffrés, le rate limiting et le Customer Portal.
|
🔨 **Volet `manager-service` livré le 2026-08-12** — les quatre chantiers de dette backend sont fermés en une passe : journalisation des sections, plafond de 5 utilisateurs, `GetSummary` en SQL, tests du vector store. `dotnet test` **163 → 200**. Restent l'aperçu de conversation (L9), l'onglet « Vocal », les restes non chiffrés, le rate limiting et le Customer Portal.
|
||||||
|
|
||||||
|
✅ **Volet `manager-service` terminé le 2026-08-13.** Customer Portal livré, ratio jetons → questions calé sur du réel ; le rate limiting, l'endpoint `ApplicationInstance` et les quotas seed **étaient déjà faits** et n'attendaient qu'une vérification (détail dans les lignes concernées). `dotnet build` vert, `dotnet test` **211 au total : 197 passés, 14 sautés** (Testcontainers, pas de démon Docker), 0 échec.
|
||||||
|
|
||||||
|
**Ce qui reste au lot F, et dans quel repo** — le backend ne bloque plus rien :
|
||||||
|
- ~~`manager-app`~~ ✅ **livré le 2026-08-13** : bouton du Customer Portal et diviseur `aiTokensPerQuestion`. `flutter analyze lib` **sans erreur**, `flutter build web` ✅. Détail ci-dessous.
|
||||||
|
- `mymuseum-visitapp` : déclenchement proactif (+ M3), miroir de la conversation vocale, émission des événements vocaux. C'est désormais **tout le reste du lot F**, et le plus gros.
|
||||||
|
- Non commencé, indépendant : l'aperçu de conversation (**L9**).
|
||||||
|
|
||||||
|
**Volet `manager-app` du 2026-08-13 — deux décisions prises en écrivant, à ne pas re-trancher :**
|
||||||
|
|
||||||
|
⚠️ **Le bouton du portail ne remplace pas celui du Checkout, il prend sa place quand l'essai est fini.** L'écran n'affichait rien du tout hors essai (`if (isTrialActive)` enveloppait le seul bouton) : un client payant arrivait sur un écran sans action. C'est maintenant Checkout **pendant** l'essai, portail **après** — les deux ne coexistent jamais, souscrire deux fois n'a pas de sens.
|
||||||
|
|
||||||
|
⚠️ **Le 409 a son propre message, et c'est le point qui se serait vu en prod.** Les 4 instances de la bascule viennent de Mongo et n'ont pas de `StripeCustomerId` : leur montrer « réessayez plus tard » les ferait attendre quelque chose qui n'arrivera jamais. Le message dit que leur facturation passe directement par nous. **4 clés i18n FR/EN/NL.**
|
||||||
|
|
||||||
|
⚠️ **Le diviseur vient du serveur, et son absence masque la ligne au lieu de mentir.** `guide_ia_screen` appelle `GET /api/Instance/{id}/quota` par `http` direct — le motif déjà établi dans cet écran pour `knowledge` et `insights`, un agrégat en lecture seule ne justifiant pas d'étendre `manager_api_new`. Si l'appel échoue, `~N questions` **disparaît** : sur une jauge qui sert à décider d'un achat, pas de chiffre vaut mieux qu'un chiffre faux — c'est exactement ce que le `/1000` faisait depuis le début.
|
||||||
|
|
||||||
| Quoi | Lien |
|
| Quoi | Lien |
|
||||||
|---|---|
|
|---|---|
|
||||||
| ~~Volet RGPD~~ | **Déplacé au lot J**, fin de backlog (décidé le 2026-08-11) |
|
| ~~Volet RGPD~~ | **Déplacé au lot J**, fin de backlog (décidé le 2026-08-11) |
|
||||||
@ -191,9 +206,9 @@ Ne change pas : la popup de traduction (un niveau justifié, langues verticales,
|
|||||||
| ~~**`WeatherSyncService` inondait le journal**~~ | ✅ **Corrigé le 2026-08-12 — régression du correctif ci-dessus, trouvée en creusant « pourquoi un index ? ».** `WeatherSyncService` écrit `section.WeatherResult` sur cron, à 6 h et à 13 h. Le job était bénin tant que les sections n'étaient pas auditées ; depuis, chaque rafraîchissement produisait une ligne portant la **prévision OpenWeather complète en avant ET en après** — quelques dizaines de Ko, deux fois par jour, par section météo. Le stockage est le moindre problème : ce bruit **noie les modifications humaines** que l'écran existe pour montrer. Correctif : une liste de colonnes machine (`WeatherResult`, `WeatherUpdatedDate`, `DateUpdate`) exclues du journal, et **aucune ligne produite** quand une modification ne touche qu'elles. `DateUpdate` y est pour une raison distincte — estampillé à chaque `SaveChanges`, il figurait dans tous les diffs sans rien y apprendre ; le signal était là, un test devait l'écarter à la main pour rester lisible. Vérifié : `AgendaSyncService` écrit des `EventAgenda`, non audités, et ne touche pas la ligne `Section` |
|
| ~~**`WeatherSyncService` inondait le journal**~~ | ✅ **Corrigé le 2026-08-12 — régression du correctif ci-dessus, trouvée en creusant « pourquoi un index ? ».** `WeatherSyncService` écrit `section.WeatherResult` sur cron, à 6 h et à 13 h. Le job était bénin tant que les sections n'étaient pas auditées ; depuis, chaque rafraîchissement produisait une ligne portant la **prévision OpenWeather complète en avant ET en après** — quelques dizaines de Ko, deux fois par jour, par section météo. Le stockage est le moindre problème : ce bruit **noie les modifications humaines** que l'écran existe pour montrer. Correctif : une liste de colonnes machine (`WeatherResult`, `WeatherUpdatedDate`, `DateUpdate`) exclues du journal, et **aucune ligne produite** quand une modification ne touche qu'elles. `DateUpdate` y est pour une raison distincte — estampillé à chaque `SaveChanges`, il figurait dans tous les diffs sans rien y apprendre ; le signal était là, un test devait l'écarter à la main pour rester lisible. Vérifié : `AgendaSyncService` écrit des `EventAgenda`, non audités, et ne touche pas la ligne `Section` |
|
||||||
| ~~`AuditLog` n'a pas d'index sur `Timestamp`~~ | ⛔ **Fausse alerte, retirée le 2026-08-12.** Signalée par réflexe sur « la table va grossir » — elle allait grossir à cause de la météo, pas des humains. Le flot machine coupé, il reste les éditions réelles : de l'ordre de quelques dizaines de milliers de lignes par an sur 4 clients, qu'un `ORDER BY Timestamp DESC LIMIT 50` trie en millisecondes sans index. **Donc pas d'index, pas de migration, et le gel du lot B n'est pas remis en cause** |
|
| ~~`AuditLog` n'a pas d'index sur `Timestamp`~~ | ⛔ **Fausse alerte, retirée le 2026-08-12.** Signalée par réflexe sur « la table va grossir » — elle allait grossir à cause de la météo, pas des humains. Le flot machine coupé, il reste les éditions réelles : de l'ordre de quelques dizaines de milliers de lignes par an sur 4 clients, qu'un `ORDER BY Timestamp DESC LIMIT 50` trie en millisecondes sans index. **Donc pas d'index, pas de migration, et le gel du lot B n'est pas remis en cause** |
|
||||||
| **La bascule écrira ~2 700 lignes d'audit dans la transaction globale** | ⚠️ **Vu le 2026-08-12.** 2 374 ressources + 307 sections + le reste, chacune avec un instantané JSON complet, dans la transaction unique posée par l'écart (h) du lot G. Pas fatal, mais c'est **neuf pour les sections** et c'est la transaction qui ne doit pas échouer. À surveiller au prochain dry run |
|
| **La bascule écrira ~2 700 lignes d'audit dans la transaction globale** | ⚠️ **Vu le 2026-08-12.** 2 374 ressources + 307 sections + le reste, chacune avec un instantané JSON complet, dans la transaction unique posée par l'écart (h) du lot G. Pas fatal, mais c'est **neuf pour les sections** et c'est la transaction qui ne doit pas échouer. À surveiller au prochain dry run |
|
||||||
| Restes non chiffrés : ratio jetons → questions (`/1000`) calé sur du réel, endpoint de mise à jour d'`ApplicationInstance` pour rendre les canaux activables, quotas seed 5M/20M ajustés | §1quater |
|
| ~~Restes non chiffrés~~ | ✅ **Traités le 2026-08-13 — et deux des trois étaient déjà faits.**<br>**Ratio jetons → questions** : livré côté serveur. `InstanceQuotaDTO.aiTokensPerQuestion` est désormais **mesuré** sur les `VisitorQuestion.TokensUsed` de l'instance, avec un seuil de 20 questions avant de faire foi — sans ce seuil, une seule réponse citant un long article doublerait la moyenne et le crédit restant affiché changerait de moitié d'un rafraîchissement à l'autre. En dessous du seuil, repli sur l'hypothèse de la grille tarifaire.<br>⚠️ **Le `/1000` de manager-app était faux d'un facteur 10, et c'est un chiffre montré au client.** `guide_ia_screen.dart:428` fait `(used / 1000)`, alors que la grille vend Premium à 20 M de jetons pour ~2 000 questions, soit **10 000 jetons par question** (`MyInfoMateDbContext:619`). Le gestionnaire lisait donc un crédit restant **dix fois trop généreux**. Le repli du serveur est calé sur 10 000, pas sur 1 000. **Reste à faire dans `manager-app`** (session séparée) : diviser par `aiTokensPerQuestion` au lieu de la constante.<br>⛔ **Endpoint de mise à jour d'`ApplicationInstance` : il existe déjà.** `ApplicationInstanceController` porte un CRUD complet — `PUT /api/ApplicationInstance` (`:115`) et `POST` (`:75`) — et `InstanceController.Updateinstance` écrit déjà `IsMobile`/`IsTablet`/`IsWeb` (`:209-211`). Activer un canal est donc **déjà faisable par l'API** ; ce qui manque est l'écran SuperAdmin, explicitement parqué en V2 au §5. Rien à écrire ici.<br>⛔ **Quotas seed 5M/20M : déjà ajustés** par `AlignSubscriptionPlansWithPricing` — le seed dit Essentiel 0, Pro 0, Premium 20 M, Enterprise `long.MaxValue`. Le « 5M » de cette ligne datait d'avant l'alignement | §1quater |
|
||||||
| **Rate limiting sur les API Keys** | ✅ **Confirmé V1 le 2026-08-12.** `AddRateLimiter` (natif ASP.NET Core 8), politique partitionnée par `InstanceId` sur les routes IA. **La cible n'est pas l'abus générique mais le quota de jetons** : `/api/AI/chat` est le seul endpoint qui coûte de l'argent réel, et la clé qui y donne accès est publique par construction |
|
| ~~**Rate limiting sur les API Keys**~~ | ✅ **Livré — constaté le 2026-08-13**, la ligne du 12/08 ne notait qu'une décision. Tout est en place : politique `ai` partitionnée par `InstanceId` (`Startup.cs:279-299`), `app.UseRateLimiter()` (`:344`) et les **deux** endpoints décorés (`AiController:312/349`). Plafond 120/min, 429 + `Retry-After: 60`.<br>⚠️ **L'ordre du pipeline est un choix, pas un hasard** — un commentaire le dit dans `Startup.cs` : le limiteur est **après `UseCors`** (sinon un 429 sans en-têtes CORS s'affiche comme une erreur CORS dans le navigateur et `visitapp-web` ne voit jamais le vrai code) et **après `UseAuthentication`** (sinon la partition n'a pas encore le claim d'instance et tout le monde tombe dans le même seau). Ne pas réordonner |
|
||||||
| **Stripe Customer Portal** | ✅ **Confirmé V1 le 2026-08-12.** Endpoint créant une session de portail (`Stripe.BillingPortal`) + bouton dans l'écran `Screens/Billing/subscription_screen.dart`, **qui existe déjà** mais ne sait que lancer un Checkout de souscription. Stripe héberge la page, il n'y a pas d'UI de paiement à écrire |
|
| ~~**Stripe Customer Portal**~~ | 🔨 **Backend livré le 2026-08-13.** `StripeService.CreateBillingPortalSessionAsync` + `POST /api/onboarding/billing-portal`, calqué sur `checkout-session` : Stripe héberge la page, on ne renvoie qu'une URL de redirection. **2 tests** (les deux gardes ; l'appel Stripe lui-même part sur le réseau et n'est pas couvert).<br>⚠️ **Une garde qui n'était pas dans l'énoncé : `StripeCustomerId` peut être nul.** Les 4 instances de la bascule viennent de Mongo et n'ont **jamais vu Stripe** — `CreateCustomerAsync` n'est appelée que par l'inscription self-service (`OnboardingController:224`). Sans la garde, elles seraient parties chercher un portail pour un client inexistant et auraient reçu une erreur d'API Stripe illisible côté front. C'est un **409**, distinct du 404 d'instance inconnue.<br>**Reste dans `manager-app`** (session séparée) : le bouton dans `Screens/Billing/subscription_screen.dart`, qui ne sait toujours que lancer un Checkout de souscription |
|
||||||
| ~~Tests du vector store via `Testcontainers.PostgreSql`~~ | ✅ **Livré le 2026-08-12 — et deux résultats contredisent ce que cette ligne supposait.** 14 tests sur l'image de `Deployment/Dockerfile.postgres`, donc le même PostGIS + pgvector que la prod ; ils se sautent proprement (`SkippableFact`) sans démon Docker — vérifié : 186 passés / 14 sautés / 0 échec.<br>**Établi** : pgvector **0.8.6**, `SET hnsw.iterative_scan` est accepté — ce SET est posé à chaque recherche par `SearchAsync` et n'existe qu'à partir de 0.8, donc sur une version antérieure **toute recherche échouait en production** ; les extensions `vector` et `postgis` sont bien créées par les migrations ; et à deux instances, l'une saturant l'axe de la question à **30 contre 1**, `SearchAsync` rend le bon nombre de résultats, tous de la bonne instance, **aucune fuite d'un client vers un autre**.<br>⚠️ **1. Ce qui protège du post-filtrage n'est PAS le parcours itératif, c'est l'index sur `(InstanceId, ContentType)`.** Le planificateur filtre par instance d'abord et trie exactement : l'index HNSW n'est jamais touché, donc il n'y a rien à post-filtrer. Vérifié à 620 lignes (test) et à **22 000** hors suite, avec 2 000 lignes pour l'instance cible. Un test fige ce plan — si cet index disparaissait, la recherche se dégraderait **sans qu'aucune erreur ne le dise**.<br>⚠️ **2. Et si on retire cet index, `hnsw.iterative_scan = relaxed_order` ne rattrape rien** : le parcours s'épuise après ~335 lignes (`Rows Removed by Filter` à l'`EXPLAIN ANALYZE`) sans atteindre l'instance minoritaire, et rend **zéro**. Le même jeu de données avec l'index HNSW construit **après** l'insertion rend bien ses 20 lignes : c'est la **connectivité du graphe** qui décide, et nos migrations créent l'index sur une table vide. Le SET de `SearchAsync` est correct et sans coût — on le garde — mais **il ne couvre pas ce qu'on croyait**.<br>**Outillage** : `Testcontainers.PostgreSql` épinglé en **3.10.0** (la 4.x parle l'API Docker 1.44, l'engine local plafonne à 1.43) et l'image construite par le **CLI docker**, pas par le constructeur d'images de Testcontainers — il relit le `FROM` pour pré-tirer l'image de base et ne sait pas parser `tag@sha256:`. Ce digest protège la base d'un changement de glibc sous ses index : il ne se retire pas pour arranger un test | **L14** |
|
| ~~Tests du vector store via `Testcontainers.PostgreSql`~~ | ✅ **Livré le 2026-08-12 — et deux résultats contredisent ce que cette ligne supposait.** 14 tests sur l'image de `Deployment/Dockerfile.postgres`, donc le même PostGIS + pgvector que la prod ; ils se sautent proprement (`SkippableFact`) sans démon Docker — vérifié : 186 passés / 14 sautés / 0 échec.<br>**Établi** : pgvector **0.8.6**, `SET hnsw.iterative_scan` est accepté — ce SET est posé à chaque recherche par `SearchAsync` et n'existe qu'à partir de 0.8, donc sur une version antérieure **toute recherche échouait en production** ; les extensions `vector` et `postgis` sont bien créées par les migrations ; et à deux instances, l'une saturant l'axe de la question à **30 contre 1**, `SearchAsync` rend le bon nombre de résultats, tous de la bonne instance, **aucune fuite d'un client vers un autre**.<br>⚠️ **1. Ce qui protège du post-filtrage n'est PAS le parcours itératif, c'est l'index sur `(InstanceId, ContentType)`.** Le planificateur filtre par instance d'abord et trie exactement : l'index HNSW n'est jamais touché, donc il n'y a rien à post-filtrer. Vérifié à 620 lignes (test) et à **22 000** hors suite, avec 2 000 lignes pour l'instance cible. Un test fige ce plan — si cet index disparaissait, la recherche se dégraderait **sans qu'aucune erreur ne le dise**.<br>⚠️ **2. Et si on retire cet index, `hnsw.iterative_scan = relaxed_order` ne rattrape rien** : le parcours s'épuise après ~335 lignes (`Rows Removed by Filter` à l'`EXPLAIN ANALYZE`) sans atteindre l'instance minoritaire, et rend **zéro**. Le même jeu de données avec l'index HNSW construit **après** l'insertion rend bien ses 20 lignes : c'est la **connectivité du graphe** qui décide, et nos migrations créent l'index sur une table vide. Le SET de `SearchAsync` est correct et sans coût — on le garde — mais **il ne couvre pas ce qu'on croyait**.<br>**Outillage** : `Testcontainers.PostgreSql` épinglé en **3.10.0** (la 4.x parle l'API Docker 1.44, l'engine local plafonne à 1.43) et l'image construite par le **CLI docker**, pas par le constructeur d'images de Testcontainers — il relit le `FROM` pour pré-tirer l'image de base et ne sait pas parser `tag@sha256:`. Ce digest protège la base d'un changement de glibc sous ses index : il ne se retire pas pour arranger un test | **L14** |
|
||||||
|
|
||||||
### Lot G — `MigrationController` à niveau + dry run (3 à 4 jours) — le plus gros
|
### Lot G — `MigrationController` à niveau + dry run (3 à 4 jours) — le plus gros
|
||||||
@ -361,7 +376,7 @@ Les CGU §8 ont été réécrites le 2026-08-11 (`cgu-myinfomate.md`) et le text
|
|||||||
|
|
||||||
> ⚠️ **À arbitrer avant le lot H** : Pro n'inclut pas l'IA, donc rien ne s'indexera pour MDLF et le Fort et leur guide ne répondra jamais. Sur 4 instances, la chaîne RAG ne serait éprouvée que sur 2 — or le lien **L14** demande justement de tester le post-filtrage HNSW **à deux instances au moins**. Deux sorties : les passer en Premium le temps des tests, ou leur poser un quota IA à la main (l'add-on ci-dessus), en pensant au rattrapage d'indexation.
|
> ⚠️ **À arbitrer avant le lot H** : Pro n'inclut pas l'IA, donc rien ne s'indexera pour MDLF et le Fort et leur guide ne répondra jamais. Sur 4 instances, la chaîne RAG ne serait éprouvée que sur 2 — or le lien **L14** demande justement de tester le post-filtrage HNSW **à deux instances au moins**. Deux sorties : les passer en Premium le temps des tests, ou leur poser un quota IA à la main (l'add-on ci-dessus), en pensant au rattrapage d'indexation.
|
||||||
|
|
||||||
**4. Écart restant, hors bascule** : la FAQ de `myinfomate-landing` (`src/data/segments.ts`) appelle encore le plan à 179€ « **Bundle** » — **41 occurrences en 4 langues** — alors que les cartes de tarifs disent « Premium ». Un prospect qui lit la grille puis la FAQ voit deux noms pour la même offre. Chantier de texte commercial, sans effet sur la migration.
|
**4. ~~Écart restant, hors bascule~~** ✅ **Corrigé le 2026-08-13.** La FAQ de `myinfomate-landing` (`src/data/segments.ts`) appelait le plan à 179 € « **Bundle** » alors que les cartes de tarifs disent « Premium » : **41 occurrences en 4 langues** (FR, EN, NL, DE) passées en « Premium ». Vérifié : plus aucun « Bundle » dans `myinfomate-landing/src`, et les formes composées des langues germaniques suivent (`Bundle-abonnementen` → `Premium-abonnementen`, `Bundle-Pläne` → `Premium-Pläne`). `npm run build` vert. Aucun identifiant touché — les 41 occurrences étaient toutes de la prose.
|
||||||
|
|
||||||
| I7 | **Recette de bascule — [test-plan.md §22](test-plan.md)**, ajoutée le 2026-08-11. Comptages globaux, puis **par type de section**, collections filles, colonnes générées, et comparaison à l'écran ancienne app vs nouvelle. ⚠️ **À jouer pendant que Mongo est encore lisible** : c'est la seule fenêtre où les deux bases coexistent, après la comparaison est impossible | Le dry run compare des **comptages**, le §22 compare le **contenu** — un compte juste ne dit pas qu'un quiz a gardé ses questions ni qu'un article a gardé ses traductions. Les écarts b et c ne se voient qu'ici |
|
| I7 | **Recette de bascule — [test-plan.md §22](test-plan.md)**, ajoutée le 2026-08-11. Comptages globaux, puis **par type de section**, collections filles, colonnes générées, et comparaison à l'écran ancienne app vs nouvelle. ⚠️ **À jouer pendant que Mongo est encore lisible** : c'est la seule fenêtre où les deux bases coexistent, après la comparaison est impossible | Le dry run compare des **comptages**, le §22 compare le **contenu** — un compte juste ne dit pas qu'un quiz a gardé ses questions ni qu'un article a gardé ses traductions. Les écarts b et c ne se voient qu'ici |
|
||||||
| I8 | Bascule DNS / API, Mongo gardé en lecture seule quelques jours, `pg_dump` après | ⚠️ **Remplacé par la stratégie de coexistence ci-dessous, décidée le 2026-08-12** |
|
| I8 | Bascule DNS / API, Mongo gardé en lecture seule quelques jours, `pg_dump` après | ⚠️ **Remplacé par la stratégie de coexistence ci-dessous, décidée le 2026-08-12** |
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user