diff --git a/STATUS.md b/STATUS.md index 60d7b44..1b059b5 100644 --- a/STATUS.md +++ b/STATUS.md @@ -315,6 +315,8 @@ Les lots 1 et 2 sont terminés. **Le lot 3 et le lot médias sont tous les deux - **(f)** `IsQRCode`/`IsSearchText`/`IsSearchNumber` n'existent pas dans Mongo — `false` est le bon défaut. Contrôle inverse fait : les cinq réglages qui *existent* (`IsDate`, `IsHour`, `IsSectionImageBackground`, `RoundedValue`, `ScreenPercentageSectionsMainPage`) sont bien mappés sur `AppConfigurationLink`. - **(g)** tombé avec le rename du lot B. +**Recette de bascule** : [test-plan.md §22](test-plan.md) — comparaison contenu par contenu entre l'app d'aujourd'hui et la base migrée, à jouer **pendant que Mongo est encore lisible**. Reliée depuis I7. + **Dry run** : automatisé en test (`MigrationDryRunTests`, se saute sans Mongo), joué sur l'export du 1ᵉʳ avril. Il a trouvé du premier coup **20 sections manquantes avec `Erreurs : 0`** — elles référencent 2 configurations supprimées dans Mongo (contenu MDLF). Pas un défaut de migration, mais un silence : elles sont désormais signalées dans `Skipped`. ⚠️ **Décision produit en attente** : recréer les 2 configurations avant la bascule pour récupérer ce contenu, ou acter sa perte. ⚠️ **À rejouer sur un dump frais** avant le jour J — l'export a 4 mois et la prod tourne encore sur Mongo. **Les cinq vrais, corrigés** : diff --git a/test-plan.md b/test-plan.md index ec7cd3b..0b423af 100644 --- a/test-plan.md +++ b/test-plan.md @@ -761,6 +761,135 @@ Points de vigilance à ne pas oublier au moment de tester : --- +## 22. Recette de bascule — l'app d'aujourd'hui vs la base migrée + +> Ajoutée le 2026-08-11, à la demande. **À jouer juste après le `POST /api/migration/run` du jour J**, avant la bascule DNS et pendant que Mongo est encore lisible. C'est la seule fenêtre où les deux bases coexistent : après, la comparaison n'est plus possible. +> +> Le dry run compare des **comptages**. Cette recette compare le **contenu** — un compte juste ne dit pas qu'une section a gardé ses questions, ses traductions et ses médias. +> +> Rappel : `MigrationController` lit un MongoDB **vivant**, pas les fichiers de `migration-data/`. + +### 22.1 — Comptages globaux, les deux bases côte à côte + +```bash +# Mongo (source) — adapter l'URI au Mongo de prod ou au dump restauré +docker run --rm --network container:myim_mongo_dryrun mongo:6 \ + mongosh "mongodb://localhost:27017/TabletDb" --quiet --eval ' + ["Instances","Users","Configurations","Resources","Sections","Devices"] + .forEach(c => print(c.padEnd(16) + db[c].countDocuments()));' +``` + +```bash +# Postgres (cible) +docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL' +SELECT 'Instances' AS entite, count(*) FROM "Instances" +UNION ALL SELECT 'Users', count(*) FROM "Users" +UNION ALL SELECT 'Configurations', count(*) FROM "Configurations" +UNION ALL SELECT 'Resources', count(*) FROM "Resources" +UNION ALL SELECT 'Sections', count(*) FROM "Sections" +UNION ALL SELECT 'Devices', count(*) FROM "Devices"; +SQL +``` + +| # | Contrôle | Attendu | OK | +|---|---|---|---| +| 22.1.1 | Instances, Users, Configurations, Resources, Devices | **Identiques** des deux côtés | | +| 22.1.2 | Sections | Postgres = Mongo **moins les orphelines signalées**. Le rapport les liste dans `Skipped` : leur nombre doit expliquer *exactement* l'écart | | +| 22.1.3 | `FatalError` et code HTTP | Vide, et **200**. Un 500 veut dire que la transaction a été annulée : rien n'a été écrit, on corrige et on rejoue | | + +### 22.2 — Sections par type : c'est là que les pertes se cachent + +Un compte global juste peut masquer 19 articles perdus compensés ailleurs. À comparer **type par type**. + +```bash +# Mongo — Type est un entier : 0=Map 1=Slider 2=Video 3=Web 4=Menu 5=Quiz +# 6=Article 7=PDF 8=Game 9=Agenda 10=Weather +docker run --rm --network container:myim_mongo_dryrun mongo:6 \ + mongosh "mongodb://localhost:27017/TabletDb" --quiet --eval ' + db.Sections.aggregate([{$group:{_id:"$Type",n:{$sum:1}}},{$sort:{_id:1}}]) + .forEach(r => print(r._id + " : " + r.n));' +``` + +```bash +# Postgres — héritage TPH, le type vit dans Discriminator +docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL' +SELECT "Discriminator", count(*) FROM "Sections" GROUP BY 1 ORDER BY 1; +SQL +``` + +| # | Contrôle | Attendu | OK | +|---|---|---|---| +| 22.2.1 | Chaque type de Mongo se retrouve en Postgres | Écart nul, **ou** entièrement expliqué par les orphelines de ce type | | +| 22.2.2 | `Event` et `Parcours` | **0 des deux côtés** — ces types n'existaient pas dans Mongo. En voir apparaître signalerait une erreur de mapping | | +| 22.2.3 | `Game` | Autant que de type 8 dans Mongo, avec `GameType = Puzzle` | | + +### 22.3 — Les collections filles, invisibles dans les comptages de sections + +C'est l'écart (b) : un quiz peut arriver sans une seule question sans que rien ne le signale. + +```bash +docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL' +SELECT count(*) AS questions, + count(DISTINCT "SectionQuizId") AS quiz_pourvus +FROM "QuizQuestions"; +SQL +``` + +| # | Contrôle | Attendu | OK | +|---|---|---|---| +| 22.3.1 | Total `QuizQuestions` | **41** sur l'export d'avril — à recalculer sur le dump du jour J | | +| 22.3.2 | Ouvrir un quiz migré dans manager-app | Ses questions sont là, **avec leurs réponses et la bonne réponse cochée** | | +| 22.3.3 | Ordre des réponses | Identique à l'ancienne app | | +| 22.3.4 | `EventAgendas` et `GuidedPaths` | **Vides** — Mongo n'en contient pas. Non vides = un mapping a inventé des données | | + +### 22.4 — Ce que la migration génère ou décide + +Ces colonnes n'existent pas dans Mongo : rien à confronter, tout à vérifier. **C'est ici que se logent les pannes silencieuses du §1quinquies.** + +```bash +docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL' +SELECT "Name", "WebSlug", left("PublicApiKey", 6) AS cle, "SubscriptionPlanId", + "AiTokensPerMonth", "IsMobile", "IsTablet", "IsWeb", "IsAssistant" +FROM "Instances" ORDER BY "Name"; +SQL +``` + +| # | Contrôle | Attendu | OK | +|---|---|---|---| +| 22.4.1 | `PublicApiKey` | **Non nulle pour les 4 instances**, préfixe `ap_`. Nulle = les apps visiteur ne s'authentifient plus | | +| 22.4.2 | `WebSlug` | Non nul et **unique**, dérivé du nom | | +| 22.4.3 | `SubscriptionPlanId` | `plan-premium` pour MyInfoMate et VisitNamur, `plan-pro` pour MDLF et le Fort | | +| 22.4.4 | `AiTokensPerMonth` | > 0 sur les deux Premium. **0 sur les deux Pro, c'est voulu** — Pro n'inclut pas l'IA | | +| 22.4.5 | `IsMobile` / `IsTablet` | Cohérents avec les apps réellement utilisées par chaque client | | +| 22.4.6 | `IsWeb` | **Faux partout** — le canal web n'existait pas avant, il s'active à la main | | +| 22.4.7 | Rôles des utilisateurs | Tous `InstanceAdmin`. **Aucun `SuperAdmin` : à poser à la main sur ton compte**, sinon les écrans SuperAdmin sont inatteignables | | +| 22.4.8 | `Resources.StoragePath` | Renseigné pour les types fichier, **nul pour les types URL** (`ImageUrl`, `VideoUrl`, `JSONUrl`) | | +| 22.4.9 | `Resources.SizeBytes` à 0 | Uniquement celles listées dans les `Errors` du rapport (HEAD en échec). Le quota les comptera pour rien tant qu'un backfill ne passe pas | | + +### 22.5 — Comparaison à l'écran, ancienne app vs nouvelle + +Les comptages ne disent rien des traductions, des médias liés ni de l'ordre. **Prendre une configuration par instance** et les ouvrir côte à côte. + +| # | Contrôle | Attendu | OK | +|---|---|---|---| +| 22.5.1 | La liste des sections, dans l'ordre | Même ordre, mêmes libellés | | +| 22.5.2 | Une section Article | Contenu HTML identique, **dans chaque langue de la configuration** | | +| 22.5.3 | Une SectionMap | Mêmes points, mêmes catégories, même icône, même centrage et zoom | | +| 22.5.4 | Un Menu | Les sections liées sont les mêmes (`LinkMenuSectionsAsync` relie après coup) | | +| 22.5.5 | Un PDF | Le fichier s'ouvre, et c'est le bon | | +| 22.5.6 | Une sous-section | Toujours rattachée au bon parent | | +| 22.5.7 | Couleurs et loader de la configuration | Identiques | | +| 22.5.8 | **Une app visiteur avec sa vraie clé API** | Se connecte et affiche le contenu — le seul contrôle qui teste `PublicApiKey` de bout en bout | | + +### 22.6 — Le contenu qu'on sait perdu + +| # | Contrôle | Attendu | OK | +|---|---|---|---| +| 22.6.1 | Relire les `Skipped` du rapport | Chaque ligne est une section orpheline **connue et acceptée**. Une surprise ici = on arrête et on regarde | | +| 22.6.2 | Les 20 sections MDLF orphelines | Soit récupérées (les 2 configurations recréées dans Mongo **avant** la bascule), soit leur perte **actée avec le client** | | + +--- + ## Hors scope (non implémenté) | Feature | Statut | diff --git a/v1-plan.md b/v1-plan.md index 8bc98cc..f934b4c 100644 --- a/v1-plan.md +++ b/v1-plan.md @@ -306,7 +306,7 @@ Les CGU §8 ont été réécrites le 2026-08-11 (`cgu-myinfomate.md`) et le text **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. -| I7 | Vérifications post-bascule : login manager-app, une app visiteur **avec sa clé API**, un parcours, une carte, un PDF, un quiz avec ses questions | 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 | | ### Après la prod