Docs du 12/08 : lot K découvert, lot F livré côté manager-app
Deux chantiers menés en parallèle, consignés dans la même passe parce que l'index de ce repo est partagé et que leurs modifications s'entrelacent dans STATUS.md, v1-plan.md et kanban.html. Lot K — les apps visiteur parlent l'ancien contrat. Découvert en lançant le premier vrai flutter build apk sur tablet-app depuis des mois, la commande que trois documents disaient « à reconfirmer ». Le diagnostic « un seul défaut, purement Gradle, peut-être réglé par 5a6701d » était doublement faux : six crans d'outillage Android (Gradle 7.5 → 8.11.1, AGP 7.2.0 → 8.9.1, Kotlin 2.0.10, option AGP supprimée, jcenter mort), corrigés le 12/08, puis 132 erreurs Dart que le Gradle masquait. Elles tiennent en trois causes — les cinq champs de ConfigurationDTO migrés vers AppConfigurationLinkDTO, GeoPointDTO.latitude/longitude devenus GeometryDTO geometry avec PostGIS, et les min() sur dynamic qui en découlent. mymuseum-visitapp a déjà fait ce portage : c'est une copie, pas une conception. Bloquant de la bascule (lien L19) : les tablettes en service chez MDLF et au Fort ne s'arrêteraient pas proprement le jour J, elles dégraderaient sans erreur visible. +3 à 5 jours de travail déjà dû. Périmètre kiosk tranché : SectionParcours exclu définitivement d'une borne fixe, SectionEvent à supporter. Leçon générale ajoutée au §1bis — un build jamais lancé ne vaut pas mieux qu'un build vert supposé, et « à reconfirmer par un vrai build » veut dire que ce n'est pas confirmé. Lot F — le volet manager-app est livré. Écran d'audit log sur GET /api/Audit réservé au SuperAdmin, et compteur « X / 5 utilisateurs » avec bouton d'ajout désactivé au plafond. Deux dettes serveur ouvertes par ce chantier et laissées ouvertes à dessein, le périmètre étant manager-app seul : le plafond de 5 n'est appliqué nulle part côté serveur — ce qui est livré est un garde-fou d'interface, un POST direct sur l'API passe toujours — et aucune section n'est journalisée, AuditedTypes.Contains exigeant l'égalité exacte de type alors que Section est abstraite, si bien que le journal couvre tout sauf le contenu, précisément ce que l'écran devait tracer. Chacune a sa carte au kanban et son entrée dans todo-features.md. Compteurs du kanban recomptés : Urgent 4, Migration v3 3, Bugs 5, À tester 6, Planifié 20, Bascule prod 7, Fait récemment 36. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
acf6ea4344
commit
d6501252a8
58
STATUS.md
58
STATUS.md
@ -32,10 +32,14 @@ Résumé de la table de suivi de ce fichier :
|
||||
| manager-app | ✅ `flutter build web` | débloqué : `// @dart=2.18` manquant dans `manager_api_new/lib/api/onboarding_api.dart` (fichier ajouté à la main pour l'onboarding, sans l'annotation de version que porte `api.dart`) — **cassait les 3 apps Flutter d'un coup** |
|
||||
| mymuseum-visitapp | ❌ `flutter build apk` (**revérifié le 2026-08-11**) | Était ✅ le 2026-08-06, débloqué par la même correction + typage explicite dans `guided_step_challenge.dart:83`. **Casse maintenant côté Gradle**, pas côté Dart : `ndkVersion = "28.2.13676358"` réclamé dans `android/app/build.gradle`, avertissements KGP sur 11 plugins. Côté Dart il reste 4 erreurs, **toutes sur la branche Ray-Ban** (`kElevenLabsApiKey`/`kElevenLabsVoiceId` absents de `constants.dart`, jamais committés — voir §5bis), dans des fichiers hors du graphe de `main.dart`. Même famille que tablet-app : l'environnement Android a bougé |
|
||||
| visitapp-web | ✅ `npm run build` | débloqué : helper `getGeoPointLatLng()` dans `src/lib/geo.ts`, les 7 erreurs venaient d'un `unknown` non narrowé. ⚠️ ce type d'erreur est **invisible en `next dev`** |
|
||||
| tablet-app | ❓ | échec Gradle sur `flutter_tools/gradle/build.gradle.kts` — **seul build encore cassé** au 2026-08-06. ⚠️ **Peut-être corrigé depuis** : commit `5a6701d` « Drapeaux du migrateur Flutter (builtInKotlin, newDsl) ». À reconfirmer par un vrai build, pas par le log. **Le problème est purement Gradle** : une piste Dart (`ContentGeoPoint`) a été ouverte puis **écartée** le 2026-08-11, cf. §1sexies — ne pas la rouvrir |
|
||||
| tablet-app | ❌ `flutter build apk` — **mesuré pour de vrai le 2026-08-12** | ⛔ **Le diagnostic « un seul défaut, purement Gradle, peut-être réglé par `5a6701d` » était doublement faux.** Il a tenu des mois parce que personne n'avait lancé la commande. Réalité : **six crans d'outillage** (Gradle 7.5 → 8.11.1, AGP 7.2.0 → 8.9.1, Kotlin 1.9.0 → 2.0.10, option AGP supprimée, jcenter mort) — **corrigés le 2026-08-12** — **puis 132 erreurs Dart** que le Gradle masquait : l'app est restée sur **l'ancien contrat d'API**, celui d'avant Postgres v3. Détail complet et suite : **[v1-plan.md lot K](v1-plan.md)** |
|
||||
|
||||
**Leçon** : `flutter analyze` et `next dev` ne suffisent pas. Seuls `flutter build` / `npm run build` / `dotnet test` disent la vérité — d'où la nouvelle section §0 du plan de test.
|
||||
|
||||
**Leçon renforcée le 2026-08-12** : un build **jamais lancé** ne vaut pas mieux qu'un build vert supposé. Trois affirmations de ce tableau ont sauté d'un coup dès qu'on a tapé la commande. Corollaire à retenir : *« à reconfirmer par un vrai build »* dans une doc veut dire **que ce n'est pas confirmé**, et un point non confirmé ne doit pas être chiffré comme une demi-heure.
|
||||
|
||||
⚠️ **Et un piège de vérification à connaître** : `flutter build apk … | tail` renvoie **le code de sortie du `tail`**, pas celui du build. Un échec y ressort en « exit code 0 ». Rediriger vers un fichier et lire `$?` séparément.
|
||||
|
||||
---
|
||||
|
||||
## 1ter. Chantier « migration Postgres v3 » — ordre de bataille (ajouté le 2026-08-07)
|
||||
@ -308,6 +312,20 @@ Les lots 1 et 2 sont terminés. **Le lot 3 et le lot médias sont tous les deux
|
||||
|
||||
**Chemin critique** : A (assainir) → B (geler le schéma) → G (`MigrationController`) → H (tests) → **J (RGPD)** → I (bascule). C, D, D-bis, E, F se parallélisent.
|
||||
|
||||
### Lot K — les apps visiteur parlent l'ancien contrat (ajouté le 2026-08-12)
|
||||
|
||||
**Un lot entier qui manquait, et il bloque la bascule.** Découvert en lançant le premier vrai `flutter build apk` sur `tablet-app` depuis des mois — la commande que trois documents disaient « à reconfirmer ».
|
||||
|
||||
**Deux couches, pas une** : six crans d'outillage Android (**corrigés le 12/08**, détail dans [v1-plan.md lot K](v1-plan.md) — Gradle 7.5 → 8.11.1, AGP 7.2.0 → 8.9.1, Kotlin 2.0.10, jcenter mort), **puis 132 erreurs Dart** que le Gradle masquait depuis le début.
|
||||
|
||||
**Les 132 erreurs tiennent en 3 causes** : `ConfigurationDTO.roundedValue`/`isDate`/`isHour`/`isSectionImageBackground`/`screenPercentageSectionsMainPage` ont migré vers **`AppConfigurationLinkDTO`** (73) · `GeoPointDTO.latitude/longitude` sont devenus **`GeometryDTO geometry`** avec PostGIS (40) · les `min()` sur `dynamic` en découlent (19). **`mymuseum-visitapp` a déjà fait ce portage** (`visitAppContext.currentAppConfigurationLink`, `Models/visitContext.dart`) : c'est une copie, pas une conception. Côté `visitapp-web`, le pendant existe aussi — le helper `getGeoPointLatLng()` de `src/lib/geo.ts`.
|
||||
|
||||
⚠️ **Ce qui en fait un bloquant de la bascule — lien L19, que le plan ne portait nulle part.** Les tablettes en service chez MDLF et au Fort parlent l'ancien contrat. Le jour J, elles ne s'arrêteraient pas proprement : cartes sans points, écrans dégradés, **aucune erreur visible**. Et le parc des téléphones visiteurs, lui, ne se force pas — d'où la stratégie de coexistence adoptée au lot I (deux serveurs, l'ancienne prod figée, aucun DNS qui bouge).
|
||||
|
||||
**Périmètre kiosk tranché le 2026-08-12** : `tablet-app` couvre 11 des 13 types. `SectionParcours` est **exclu définitivement** (un parcours guidé fait marcher le visiteur — pas de sens sur borne fixe) ; `SectionEvent` est **à supporter** (borne d'accueil affichant le programme du jour, §19.13 cas E). Ni l'un ni l'autre n'est un retard : **les deux types sont nés avec Postgres v3**. S'ajoute le renommage `SectionPuzzle` → `SectionGame` et le nouveau `SlidingPuzzle`, à récupérer depuis `mymuseum-visitapp/lib/Screens/Sections/Game/` — sa version est moins buguée que le `Puzzle/` de tablet-app.
|
||||
|
||||
**Ce que ça coûte** : +3 à 5 jours. Ce n'est pas du périmètre ajouté, c'est du travail déjà dû que personne n'avait vu.
|
||||
|
||||
**D1 — le bug offline nº1 est corrigé (2026-08-11).** Le `switch` de collecte des ressources était commenté **des deux côtés** : 157 lignes mortes dans `ConfigurationController.Export`, 126 dans `Import`, et le pendant dans `downloadConfiguration.dart`. Une visite téléchargée n'embarquait donc que l'image de la configuration, celle du loader et l'image de chaque section.
|
||||
|
||||
Remplacé par `GetReferencedResourceIds()`, déjà implémentée sur les 13 sous-types. Côté import et côté client, la charge est enregistrée en une passe au lieu d'être redécouverte section par section — ce qui répare au passage la purge des fichiers obsolètes, pilotée par `usedImageOrAudioIds`. **4 tests**, dont un qui vérifie par réflexion que les 13 sous-types implémentent la collecte : le trou venait d'un `switch` où un type oublié passait dans le `default` sans bruit. `dotnet test` **148/148**.
|
||||
@ -332,6 +350,17 @@ Remplacé par `GetReferencedResourceIds()`, déjà implémentée sur les 13 sous
|
||||
- **(e)** passe par `ResourceStorage` (L5) ; `StoragePath` n'était pas écrit du tout. L'échec du `HEAD` remonte désormais dans le rapport.
|
||||
- **(h)** transaction unique, et **500 au lieu de 200 OK** sur échec fatal.
|
||||
|
||||
**Lot F — le volet manager-app est livré le 2026-08-12 : écran d'audit log et compteur d'utilisateurs.** `flutter build web` ✅, `flutter analyze` propre sur les dossiers travaillés. **`manager_api_new` n'a pas été touché** — l'écran passe par un `http.get` direct au Bearer, comme `_loadKnowledge` / `_loadInsights` / `_reindex` du Guide IA : une lecture seule ne justifie pas d'étendre un client qui s'édite à la main, et le déclencheur de génération n'existe plus depuis le lot A.
|
||||
|
||||
- **Écran d'audit** : `Screens/Audit/audit_screen.dart` + `audit_entry.dart`, entrée de menu `menuId: 13` conditionnée à `role.value == 0` — même règle que la policy `SuperAdmin` de l'endpoint. Filtres instance / type d'entité / utilisateur / plage de dates, pagination 50, clic sur une ligne → détail avant-après en table. Les ids d'instance et d'utilisateur sont résolus en noms via `instanceGet()` / `userGet()`, un id inconnu de l'annuaire restant affiché tel quel : le journal doit rester lisible après la suppression de ce qu'il décrit.
|
||||
- **Compteur utilisateurs** : « X / 5 » et bouton d'ajout désactivé au plafond. **Le SuperAdmin en est exclu** — son `GET /api/User` renvoie toutes les instances, compter cette liste contre un plafond *par instance* n'aurait aucun sens. Le point « masquer le rôle SuperAdmin à un InstanceAdmin », resté en question dans `todo-features.md`, **était déjà fait** (`_allowedRoles` filtre sur `r >= callerRole`) : vérifié, pas réimplémenté.
|
||||
- **Trois pièges relevés en câblant, à ne pas re-découvrir** : `AuditController` renvoie les entités **brutes**, pas un DTO (champs d'`AuditLog` en camelCase) ; `invokeAPI` **ne lève pas** sur un code d'erreur et son résultat était ignoré dans `users_screen`, donc un e-mail déjà pris (409) ne produisait aucun message — corrigé, ce qui rendra le futur 422 du plafond visible sans retouche ; et `DropdownButtonFormField` **ne relit pas `initialValue`** sur reconstruction (`FormField.didUpdateWidget` ne traite que `forceErrorText`), donc « Réinitialiser les filtres » vidait la requête sans vider l'affichage — remplacé par un `DropdownButton` piloté.
|
||||
|
||||
⚠️ **Deux dettes backend ouvertes par ce chantier, laissées ouvertes à dessein** (le périmètre était `manager-app` seul) :
|
||||
|
||||
1. **Le plafond de 5 n'existe pas côté serveur.** `UserController.CreateUser` ne compte rien, pas de 422, et `SubscriptionPlan` ne porte aucun champ utilisateur. Ce qui est livré est un **garde-fou d'interface** : un POST direct sur l'API passe toujours. S'il devait varier par plan, c'est une colonne — donc un changement de schéma **après** le gel du lot B.
|
||||
2. **Aucune section n'est jamais journalisée.** `AuditedTypes.Contains(entry.Entity.GetType())` (`MyInfoMateDbContext.cs:126`) exige l'égalité **exacte** de type, or `Section` est **abstraite** (`Section.cs:17`) : le type runtime est toujours `SectionMap`, `SectionQuiz`… La ligne `typeof(Section)` d'`AuditedTypes` ne matche donc rien. Le journal couvre `Resource`, `Configuration`, `Device`, `User`, `Instance` — mais **pas le contenu**, précisément ce que l'usage cible voulait tracer. Correctif : `Any(t => t.IsInstanceOfType(entry.Entity))`. ⚠️ `EntityType` portera alors `SectionMap` et non `Section` : soit normaliser côté serveur, soit élargir la liste du filtre front aux 13 sous-types. Le filtre « Section » est **déjà dans l'écran** et ne rend aucune ligne d'ici là.
|
||||
|
||||
**Lot C1 — livré le 2026-08-11.** Calculateur `StoragePath`/`SizeBytes` extrait dans `Helpers/ResourceStorage.cs`, avec 13 tests (`dotnet test` **143/143**). Il ferme le lien **L5** : C2 (backfill) et l'écart (e) du lot G appelleront le même code au lieu d'en écrire deux.
|
||||
|
||||
- ⚠️ **L'extraction a prouvé la divergence qu'elle devait empêcher.** `ResourceController` a **deux** chemins de création : le chemin **multipart** écrivait `SizeBytes` mais laissait **`StoragePath` nul**, seul le chemin JSON écrivait les deux. Le §1quater annonçait « `StoragePath`/`SizeBytes` écrits à `Create` depuis le 10/08 » — c'était vrai d'un chemin sur deux. Une partie des lignes à rattraper par C2 vient de là.
|
||||
@ -351,6 +380,33 @@ Remplacé par `GetReferencedResourceIds()`, déjà implémentée sur les 13 sous
|
||||
|
||||
⚠️ **Piège de doc corrigé au passage** : le plan disait « cas PDF dans `getElementForResource` ». Cette fonction de `mymuseum-visitapp` fait un `return CachedCustomResource(...)` **avant** son `switch` — tout son switch est du code mort. Le vrai dispatcher est `CachedCustomResource`, et il en a **deux** (ressource distante / fichier local de l'offline). Suivre la doc à la lettre aurait produit un correctif inopérant et « vérifié » par une analyse verte.
|
||||
|
||||
### Add-on IA sur un plan sans IA — mode d'emploi et correctifs (2026-08-12)
|
||||
|
||||
**Le mécanisme tient sans code.** `ApplyPlanQuotas` ne repart du plan **que si `SubscriptionPlanId` change** (`InstanceController:227`) : surcharger `Instance.AiTokensPerMonth` sur une instance Pro survit, et le compteur mensuel se réarme seul sur `AiUsageMonthKey`. C'est ce qui permet de donner l'IA à MDLF et au Fort Saint-Héribert **sans les passer en Premium** — nécessaire pour éprouver le post-filtrage HNSW sur 4 instances plutôt que 2 (**L14**).
|
||||
|
||||
**Mais l'activation est une manip, pas un geste produit.** `AiTokensPerMonth` n'est exposé dans **aucun DTO** — `UpdateInstance` ne le lit jamais. Trois gestes, dans cet ordre :
|
||||
|
||||
1. `UPDATE "Instances" SET "AiTokensPerMonth" = …, "IsAssistant" = true WHERE "Id" = '…'`
|
||||
2. `IsAssistant = true` sur les `ApplicationInstance` du client — **garde séparée** (`AiController:360`), celle qu'on oublie : l'instance est dotée, le chat refuse quand même
|
||||
3. Le bouton de relance d'indexation de l'écran Guide IA (`POST /api/Ai/reindex/{id}`, SuperAdmin) — **obligatoire** : le rattrapage automatique est câblé dans `UpdateInstance` en C# (`:234`), un UPDATE SQL ne le déclenche pas, et le client paierait un guide qui ignore tout de son contenu déjà saisi
|
||||
|
||||
⚠️ **Changer son plan plus tard écrase la surcharge** et le remet à 0 jeton sans rien signaler. Pro → Premium est sans risque (Premium donne plus) ; tout autre mouvement casse l'add-on en silence.
|
||||
|
||||
⚠️ **Aucune valeur d'add-on n'est arrêtée.** Premium est à 20 M. Tant qu'un chiffre n'est pas fixé, deux clients payant la même chose n'auront pas le même quota.
|
||||
|
||||
**Facturation : rien de neuf.** `StripeService` ne connaît qu'`EssentielPriceId` (`:56`) — Pro, Premium et Enterprise sont **déjà** facturés hors Stripe, à la main. L'add-on rejoint un processus existant, il n'ajoute pas de dette.
|
||||
|
||||
**Deux correctifs livrés le 2026-08-12** — `dotnet build` vert, `dotnet test` **148/148** :
|
||||
|
||||
- **`BackfillInstanceAsync` met en file un job par section** au lieu de les traiter en boucle. L'unité de reprise devient la section : avec `[AutomaticRetry(Attempts = 2)]` posé sur la classe, un échec d'embedding à la 280ᵉ section faisait retenter le job entier, donc **ré-embedder les 279 déjà indexées, deux fois** — des appels facturés pour rien et autant de chances de retomber sur la limite de débit. Le chemin de backfill cesse d'être un chemin à part : c'est celui de l'intercepteur. Effet de bord utile : l'avancement est visible section par section dans `/hangfire`. `IngestSectionCountingAsync` disparaît, son `int` ne servait plus.
|
||||
- **Backoff sur 429/503 dans `GoogleEmbeddingService`** : 3 tentatives, `Retry-After` s'il est fourni, exponentiel sinon. Absorbe la rafale courte sans jamais remonter jusqu'à Hangfire.
|
||||
|
||||
> 📅 **Écran SuperAdmin d'activation — reporté en V2, décidé le 2026-08-12.** Exposer `AiTokensPerMonth` dans `InstanceDTO` ferait passer les trois gestes ci-dessus à un seul, et le backfill repartirait tout seul puisque `UpdateInstance` le déclenche déjà sur la transition `0 → >0`. Mais **la V1 n'a besoin d'aucun add-on** : les 4 clients de la bascule sont affectés à leur plan (§1quinquies, plans), et doter une instance à la main reste un geste interne, fait une fois, sur une base qu'on contrôle. Ce qui rend le report tenable, c'est que la procédure SQL ci-dessus est écrite : elle ne se redécouvre pas.
|
||||
>
|
||||
> **Ce qui ferait remonter ce chantier** : vendre l'add-on à un client réel **avant** la bascule. Une manip SQL sur une instance en production qui tourne n'a pas le même coût que sur une base de dev, et c'est là que le formulaire devient le moyen sûr.
|
||||
>
|
||||
> À faire d'un bloc, le jour venu, avec l'endpoint de mise à jour d'`ApplicationInstance` (canaux activables) — c'est **le même écran**. ⚠️ Mais seule la partie add-on part en V2 : **le volet canaux reste au lot F**, il est dans les restes non chiffrés de la V1.
|
||||
|
||||
### Contrôle du code du 2026-08-11 (2e passe) — ce qui a bougé depuis la rédaction du plan
|
||||
|
||||
> Tout vérifié dans le code, pas dans les docs. Deux points du lot A tombent, un point du lot A est différé, un nouveau suspect apparaît.
|
||||
|
||||
92
kanban.html
92
kanban.html
@ -454,13 +454,13 @@
|
||||
</header>
|
||||
|
||||
<section class="summary" aria-label="Chiffres clés">
|
||||
<div class="stat"><span class="n n-critical">3</span><span class="k">Urgent</span></div>
|
||||
<div class="stat"><span class="n n-info">4</span><span class="k">Migration v3</span></div>
|
||||
<div class="stat"><span class="n n-critical">4</span><span class="k">Urgent</span></div>
|
||||
<div class="stat"><span class="n n-info">3</span><span class="k">Migration v3</span></div>
|
||||
<div class="stat"><span class="n n-warn">5</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">19</span><span class="k">Planifié</span></div>
|
||||
<div class="stat"><span class="n">20</span><span class="k">Planifié</span></div>
|
||||
<div class="stat"><span class="n n-gate">7</span><span class="k">Bascule prod</span></div>
|
||||
<div class="stat"><span class="n n-good">32</span><span class="k">Fait récemment</span></div>
|
||||
<div class="stat"><span class="n n-good">36</span><span class="k">Fait récemment</span></div>
|
||||
</section>
|
||||
|
||||
<div class="filters" role="group" aria-label="Filtrer par domaine">
|
||||
@ -486,7 +486,7 @@
|
||||
|
||||
<!-- URGENT -->
|
||||
<section class="col" style="--stripe: var(--critical)">
|
||||
<div class="col-head"><h2>Urgent</h2><span class="count">3</span></div>
|
||||
<div class="col-head"><h2>Urgent</h2><span class="count">4</span></div>
|
||||
<div class="stack">
|
||||
|
||||
<article class="card" data-area="infra">
|
||||
@ -505,12 +505,22 @@
|
||||
<span class="src">STATUS.md §1bis · §5bis</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="manager">
|
||||
<div class="card-meta"><span class="tag">tablet-app</span><span class="flag f-warn">Build</span></div>
|
||||
<h3>tablet-app ne compile pas</h3>
|
||||
<p>Échec Gradle sur <code>flutter_tools/gradle/build.gradle.kts</code>. Seul build encore cassé des cinq. Le dernier APK date d'avril. <strong>Peut-être déjà réglé</strong> par le commit <code>5a6701d</code> (« drapeaux du migrateur Flutter ») — à reconfirmer par un vrai build, pas par le log.</p>
|
||||
<p><strong>Piste écartée le 11/08, à ne pas rouvrir</strong> : <code>marker_view.dart:456</code> référence <code>ContentGeoPoint</code>, une classe absente de tout graphe de compilation — de quoi croire à un second défaut, côté Dart cette fois. Mais <strong>la ligne est dans un bloc commenté</strong> (<code>/*</code> ouvert en 418), comme sa jumelle de <code>mymuseum-visitapp:367</code> (<code>/*</code> en 329). <code>flutter analyze</code> sur les deux fichiers : zéro erreur. Le problème de tablet-app est <strong>purement Gradle</strong>.</p>
|
||||
<span class="src">parity-manager-visitapp.md §6 — B3 · STATUS.md §1bis</span>
|
||||
<article class="card" data-area="manager" data-horizon="v1">
|
||||
<div class="card-meta"><span class="tag">tablet-app</span><span class="flag f-critical">Lot K — bloque la bascule</span></div>
|
||||
<h3>tablet-app est resté sur l'ancien contrat d'API</h3>
|
||||
<p><strong>Le premier vrai build, le 12/08, a fait sauter le diagnostic de cette carte.</strong> « Un seul défaut, purement Gradle, peut-être réglé par <code>5a6701d</code> » était doublement faux — il tenait parce que personne n'avait lancé la commande. L'outillage Android demandait <strong>six crans</strong> (Gradle 7.5 → 8.11.1, AGP 7.2.0 → 8.9.1, Kotlin 2.0.10, jcenter mort) : <strong>fait le 12/08</strong>. Derrière, le Gradle masquait <strong>132 erreurs Dart</strong>.</p>
|
||||
<p><strong>3 causes seulement</strong> : <code>ConfigurationDTO.roundedValue</code> & co. ont migré vers <code>AppConfigurationLinkDTO</code> (73 err.), <code>GeoPointDTO.latitude/longitude</code> sont devenus <code>GeometryDTO geometry</code> avec PostGIS (40 err.), le reste en découle. Le modèle du portage <strong>est déjà écrit dans mymuseum-visitapp</strong> (<code>currentAppConfigurationLink</code>, <code>Models/visitContext.dart</code>) — copier, pas concevoir.</p>
|
||||
<p>⚠️ <strong>Pourquoi ça bloque la bascule (L19)</strong> : les tablettes en service chez MDLF et au Fort parlent l'ancien contrat. Le jour J elles ne s'arrêteraient pas proprement — cartes sans points, écrans dégradés, aucune erreur visible.</p>
|
||||
<span class="src">v1-plan.md lot K · STATUS.md §1bis</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="manager" data-horizon="v1">
|
||||
<div class="card-meta"><span class="tag">tablet-app</span><span class="flag f-warn">Lot K · périmètre kiosk</span></div>
|
||||
<h3>Types de section manquants sur le kiosk</h3>
|
||||
<p><code>tablet-app</code> couvre <strong>11 des 13</strong> types. Ce n'est pas un retard : <code>SectionEvent</code> et <code>SectionParcours</code> <strong>sont nés avec Postgres v3</strong>, ils n'ont jamais existé dans Mongo.</p>
|
||||
<p><strong>Tranché le 12/08</strong> — <code>SectionParcours</code> : <strong>exclu définitivement</strong>, un parcours guidé fait marcher le visiteur avec géodéclenchement, ça n'a pas de sens sur borne fixe. <code>SectionEvent</code> : <strong>à supporter</strong>, une borne d'accueil qui affiche le programme du jour et le plan est le cas d'usage kiosk par excellence (§19.13 cas E).</p>
|
||||
<p>Plus le renommage <code>SectionPuzzle</code> → <code>SectionGame</code> et le nouveau <code>SlidingPuzzle</code> : <strong>reprendre <code>mymuseum-visitapp/lib/Screens/Sections/Game/</code></strong> (<code>game_page.dart</code>, <code>sliding_puzzle_piece.dart</code>), dont la version est <strong>moins buguée</strong> que le <code>Puzzle/</code> de tablet-app. Récupération, pas réécriture.</p>
|
||||
<span class="src">v1-plan.md lot K — K3, K4</span>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
@ -518,7 +528,7 @@
|
||||
|
||||
<!-- MIGRATION -->
|
||||
<section class="col" style="--stripe: var(--info)">
|
||||
<div class="col-head"><h2>Migration v3</h2><span class="count">4</span></div>
|
||||
<div class="col-head"><h2>Migration v3</h2><span class="count">3</span></div>
|
||||
<div class="stack">
|
||||
|
||||
<article class="card" data-area="backend infra">
|
||||
@ -544,13 +554,6 @@
|
||||
<span class="src">v2/media-storage-plan.md</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="infra">
|
||||
<div class="card-meta"><span class="tag">GCP</span><span class="flag f-good">5 min</span></div>
|
||||
<h3>Alerte de budget</h3>
|
||||
<p>Bucket en Europe = aucun quota gratuit, facturation dès le premier octet. L'alerte est la vraie protection contre la surprise.</p>
|
||||
<span class="src">v2/media-storage-plan.md</span>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -649,7 +652,7 @@
|
||||
|
||||
<!-- PLANIFIÉ -->
|
||||
<section class="col" style="--stripe: var(--ink-3)">
|
||||
<div class="col-head"><h2>Planifié</h2><span class="count">19</span></div>
|
||||
<div class="col-head"><h2>Planifié</h2><span class="count">20</span></div>
|
||||
<div class="stack">
|
||||
|
||||
<article class="card" data-area="commercial" data-horizon="v1">
|
||||
@ -771,11 +774,18 @@
|
||||
<span class="src">todo-features.md — AR</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="manager">
|
||||
<div class="card-meta"><span class="tag">produit</span></div>
|
||||
<h3>Écran d'audit log & gestion des users</h3>
|
||||
<p>Backend fait, front absent. Users : scoping backend fait, manque le plafond de 5 et le compteur dans l'UI.</p>
|
||||
<span class="src">todo-features.md</span>
|
||||
<article class="card" data-area="backend">
|
||||
<div class="card-meta"><span class="tag">backend</span><span class="flag f-warn">ouvert par le front du 12/08</span></div>
|
||||
<h3>Audit & plafond users — les deux dettes serveur</h3>
|
||||
<p><strong>Le front est livré (12/08), ces deux points ne le sont pas.</strong> (1) <strong>Aucune section n'est journalisée</strong> : <code>AuditedTypes.Contains(entry.Entity.GetType())</code> exige l'égalité exacte, or <code>Section</code> est <strong>abstraite</strong> — le type runtime est toujours <code>SectionMap</code>, <code>SectionQuiz</code>… donc <code>typeof(Section)</code> ne matche jamais. Le journal couvre ressources, configurations, devices, users et instances, mais <strong>pas le contenu</strong> — précisément ce que l'écran devait tracer. Correctif : <code>Any(t => t.IsInstanceOfType(…))</code>, en décidant si <code>EntityType</code> porte alors <code>SectionMap</code> ou reste normalisé à <code>Section</code>. (2) <strong>Plafond de 5 utilisateurs</strong> : <code>UserController.CreateUser</code> ne compte rien, pas de 422 — le compteur livré est un garde-fou d'interface, un POST direct sur l'API passe toujours. Le retour d'erreur du POST est déjà branché côté front : le 422 sera visible sans retouche.</p>
|
||||
<span class="src">todo-features.md · v1-plan.md lot F · STATUS.md §1sexies</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="manager" data-horizon="v2">
|
||||
<div class="card-meta"><span class="tag">produit</span><span class="flag f-warn">V2 · nouveau 12/08</span></div>
|
||||
<h3>Écran SuperAdmin — add-on IA & canaux d'une instance</h3>
|
||||
<p>Donner l'IA à un client Pro se fait en SQL : <code>AiTokensPerMonth</code> n'est dans aucun DTO, <code>IsAssistant</code> est à poser deux fois (instance <em>et</em> <code>ApplicationInstance</code>), et le rattrapage d'indexation ne partant que depuis <code>UpdateInstance</code> en C#, il faut penser au bouton de relance. Exposer le quota dans <code>InstanceDTO</code> ramènerait ça à un formulaire. <strong>Reporté en V2 le 12/08</strong> : la V1 n'a besoin d'aucun add-on — les 4 clients de la bascule sont affectés à leur plan, et l'activation manuelle reste un geste interne, fait une fois, sur une base qu'on contrôle. À faire d'un bloc avec l'endpoint de mise à jour d'<code>ApplicationInstance</code> (canaux activables) : <strong>c'est le même écran</strong>.</p>
|
||||
<span class="src">STATUS.md §1sexies — add-on IA</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="visitapp" data-horizon="v2">
|
||||
@ -849,9 +859,13 @@
|
||||
|
||||
<article class="card" data-area="infra">
|
||||
<div class="card-meta"><span class="tag">étape 21-22</span></div>
|
||||
<h3>DNS, Mongo en lecture seule, retour arrière</h3>
|
||||
<p>Bascule DNS/API, Mongo conservé en lecture seule quelques jours, et <strong>le plan de rollback écrit avant le jour J</strong> — un retour arrière qu'on improvise à 23 h n'est pas un retour arrière.</p>
|
||||
<span class="src">STATUS.md §1quinquies — étapes 21-22</span>
|
||||
<h3>Bascule par coexistence sur deux serveurs</h3>
|
||||
<p><strong>Décidé le 12/08, remplace la bascule DNS.</strong> Un second serveur porte la nouvelle prod Postgres, les <strong>nouvelles versions des apps</strong> pointent vers lui, et l'ancienne prod Mongo continue de servir les apps déjà installées. Aucun DNS ne bouge, pas de fenêtre de maintenance, et un retour arrière qui consiste à ne rien faire.</p>
|
||||
<p>C'est la seule réponse au problème que <strong>L19</strong> pose : on ne force pas la mise à jour du téléphone d'un visiteur.</p>
|
||||
<p><strong>Ce qui rend la coexistence saine — une seule source d'écriture</strong> : dès que le back-office pointe sur la nouvelle prod, l'ancienne est figée de fait. Les vieilles apps voient un contenu gelé au jour J, ce qui est tenable quelques semaines, et <strong>les deux bases ne divergent jamais</strong>.</p>
|
||||
<p>⚠️ <strong>Ce qui ne marche pas : « recopier de la nouvelle prod vers l'ancienne ».</strong> Il n'existe pas de chemin Postgres → Mongo ; <code>MigrationController</code> va dans un seul sens et écrire l'inverse coûterait autant, pour alimenter une base qu'on éteint. L'ancienne prod <strong>se fige, elle ne se synchronise pas</strong>.</p>
|
||||
<p>À tenir : une <strong>date d'extinction</strong> décidée d'avance (sinon l'ancienne prod survit trois ans), le coût d'un second serveur OVH le temps de la transition, et un <strong>plan de rollback réécrit</strong> — il ne s'agit plus de restaurer un dump mais de republier l'app qui pointe vers l'ancienne adresse, ce qui déplace le risque sur les délais de validation des stores.</p>
|
||||
<span class="src">v1-plan.md lot I — stratégie de coexistence</span>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
@ -861,9 +875,29 @@
|
||||
|
||||
<section class="done">
|
||||
<h2>Fait récemment</h2>
|
||||
<p>Trente-deux chantiers clos entre le 5 et le 11 août 2026.</p>
|
||||
<p>Trente-six chantiers clos entre le 5 et le 12 août 2026.</p>
|
||||
<div class="done-grid">
|
||||
|
||||
<div class="done-item">
|
||||
<strong>tablet-app : six crans d'outillage Android rattrapés</strong>
|
||||
<span>Le premier vrai build depuis des mois. Chaque erreur dictait le correctif suivant : Gradle <strong>7.5 → 8.11.1</strong> (le plugin Kotlin exigeait ≥ 7.6.3), AGP <strong>7.2.0 → 8.5.0</strong> et Kotlin <strong>1.9.0 → 2.0.10</strong> (<code>onVariants$default</code>), suppression de <code>android.bundle.enableUncompressedNativeLibs</code> (retirée en AGP 8.1), AGP <strong>→ 8.7.3</strong> (minimum Flutter), AGP <strong>→ 8.9.1</strong> (réclamé par <code>androidx.browser</code> et <code>core-ktx</code>), et <code>jcenter()</code> → <code>mavenCentral()</code> (jcenter est arrêté depuis 2021). ⚠️ Un <strong>bloc <code>buildscript</code> entièrement commenté</strong> déclarait AGP 8.5.0/Kotlin 2.0.10 pendant que la vraie config vivait dans <code>settings.gradle</code> en 7.2.0/1.9.0 : il a fait conclure deux fois à une configuration qui n'était pas celle du build — supprimé, les versions ne sont plus déclarées qu'à un seul endroit. C'est le <strong>troisième piège « bloc commenté »</strong> du projet. Ce que ça débloque : <code>compileSdkVersion 36</code>, sans lequel plus aucune mise à jour n'est publiable sur le store. Reste le portage Dart — voir la carte du lot K.</span>
|
||||
</div>
|
||||
|
||||
<div class="done-item">
|
||||
<strong>Écran d'audit log et compteur d'utilisateurs — le lot F côté manager-app</strong>
|
||||
<span>Deux chantiers dans la même passe, <code>manager-app</code> seul. <strong>Écran « Activité »</strong> sur <code>GET /api/Audit</code> : filtres instance / type d'entité / utilisateur / plage de dates, pagination 50, clic sur une ligne → détail avant-après en table, entrée de menu réservée à <code>role.value == 0</code> comme la policy de l'endpoint. Les ids sont résolus en noms, un id inconnu restant affiché tel quel — le journal doit rester lisible après la disparition de ce qu'il décrit. <strong>Compteur « X / 5 utilisateurs »</strong> et bouton d'ajout désactivé au plafond, <strong>SuperAdmin exclu</strong> : sa liste couvre toutes les instances, la compter contre un plafond <em>par instance</em> n'aurait aucun sens. <strong><code>manager_api_new</code> n'a pas été touché</strong> — <code>http.get</code> direct au Bearer, comme le Guide IA : une lecture seule ne justifie pas d'étendre un client qui s'édite à la main. 40 clés i18n FR/EN/NL, <code>flutter build web</code> ✅. ⚠️ <strong>Trois pièges relevés en câblant</strong> : <code>AuditController</code> renvoie les entités <strong>brutes</strong>, pas un DTO ; <code>invokeAPI</code> <strong>ne lève pas</strong> sur code d'erreur et son résultat était ignoré, donc un e-mail déjà pris (409) ne produisait aucun message — corrigé ; et <code>DropdownButtonFormField</code> <strong>ne relit pas <code>initialValue</code></strong> sur reconstruction, donc « Réinitialiser les filtres » vidait la requête sans vider l'affichage — remplacé par un <code>DropdownButton</code> piloté. Les deux dettes serveur qu'il ouvre sont sur leur propre carte, en Planifié.</span>
|
||||
</div>
|
||||
|
||||
<div class="done-item">
|
||||
<strong>Alerte de budget GCP posée</strong>
|
||||
<span>Le bucket est en Europe : aucun quota gratuit, facturation dès le premier octet. L'alerte est posée dans la console GCP — rien à vérifier dans le code, et c'est la seule protection réelle contre une surprise de facture pendant les tests de charge du lot H.</span>
|
||||
</div>
|
||||
|
||||
<div class="done-item">
|
||||
<strong>Le rattrapage d'indexation ne brûle plus le quota Gemini en boucle</strong>
|
||||
<span><code>BackfillInstanceAsync</code> parcourait les sections d'une instance <em>en boucle dans un seul job</em>. Avec <code>[AutomaticRetry(Attempts = 2)]</code> posé sur la classe, un échec d'embedding à la 280ᵉ section faisait retenter le job entier par Hangfire — donc <strong>ré-embedder les 279 déjà indexées, deux fois</strong>. Des appels facturés pour rien, et autant de chances de retomber sur la limite de débit qui avait causé le premier échec. Elle met désormais en file <strong>un job par section</strong> : l'unité de reprise devient la section, <code>ReplaceAsync</code> la rend idempotente, et le chemin de backfill cesse d'être un chemin à part — c'est celui de l'intercepteur, déjà éprouvé. Effet de bord utile : l'avancement se lit section par section dans <code>/hangfire</code> au lieu d'un job opaque. Complété par un <strong>backoff sur 429/503</strong> dans <code>GoogleEmbeddingService</code> (3 tentatives, <code>Retry-After</code> s'il est fourni, exponentiel sinon), qui absorbe la rafale courte sans jamais remonter jusqu'à Hangfire. <code>dotnet test</code> 148/148. Découvert en préparant l'activation de l'IA sur MDLF et le Fort — deux instances Pro à doter à la main pour éprouver le post-filtrage HNSW sur 4 instances plutôt que 2.</span>
|
||||
</div>
|
||||
|
||||
<div class="done-item">
|
||||
<strong>La visite hors ligne embarque enfin ses médias</strong>
|
||||
<span>Le <code>switch</code> qui collecte les ressources d'une configuration était commenté <strong>des deux côtés</strong> : 157 lignes mortes dans <code>ConfigurationController.Export</code>, 126 dans <code>Import</code>, et le pendant dans <code>downloadConfiguration.dart</code>. Une visite téléchargée n'embarquait donc que l'image de la configuration, celle du loader et l'image de chaque section — <strong>ni contenus d'articles, ni audios, ni icônes de carte, ni images de quiz</strong>. Remplacé par <code>GetReferencedResourceIds()</code>, qui existait déjà sur les 13 sous-types et n'attendait que d'être appelée. Côté import et côté client, la charge est enregistrée <em>en une passe</em> au lieu d'être redécouverte section par section : le serveur sait maintenant ce qu'il envoie, le client n'a plus à le deviner — et ça répare au passage la purge des fichiers obsolètes, pilotée par la même liste d'ids utilisés. 4 tests, dont un qui vérifie <strong>par réflexion que les 13 sous-types implémentent la collecte</strong> : le trou venait d'un <code>switch</code> où un type oublié passait dans le <code>default</code> sans bruit, et c'est exactement ce qu'il ne faut plus pouvoir refaire. <code>dotnet test</code> 148/148. ⚠️ Le volet visiteur ne tient que sur l'analyse — l'APK ne compile pas — <strong>confirmation sur device au §21</strong>, carte « Visite hors ligne sur device » déjà en attente.</span>
|
||||
|
||||
@ -485,10 +485,11 @@
|
||||
- ✅ `POST` : `InstanceId` **forcé** à l'instance de l'appelant (`l.133`), pas celui envoyé par le client
|
||||
- ✅ Protection contre l'escalade de rôle : `PUT`/`DELETE` refusés si `user.Role < GetCallerRole()` (`l.210`, `l.266`)
|
||||
|
||||
#### Ce qui reste réellement à faire
|
||||
- ❌ **Plafond de 5 utilisateurs par instance** — aucun contrôle de comptage dans `UserController`, pas de 422
|
||||
- ❌ **Manager-app** : compteur "X / 5 utilisateurs" + désactivation du bouton d'ajout au quota
|
||||
- ❓ À vérifier : le champ de rôle `SuperAdmin` est-il masqué dans le formulaire manager-app pour un `InstanceAdmin` ?
|
||||
#### Ce qui reste réellement à faire — révisé le 2026-08-12
|
||||
- ✅ **Manager-app** : compteur « X / 5 utilisateurs » + bouton d'ajout désactivé au quota — livré le 2026-08-12 (`kMaxUsersPerInstance` dans `constants.dart`, `users_screen.dart`). Le compteur et le plafond **ne s'appliquent pas au SuperAdmin** : son `GET /api/User` renvoie toutes les instances, compter cette liste contre un plafond *par instance* n'aurait aucun sens
|
||||
- ✅ **Le rôle `SuperAdmin` est déjà masqué** pour un `InstanceAdmin` — la question était ouverte, elle est fermée : `_allowedRoles` filtre sur `r >= callerRole` (`users_screen.dart:40`), un appelant de rôle 1 ne se voit proposer que 1/2/3. C'était fait, jamais consigné
|
||||
- ❌ **Plafond de 5 utilisateurs par instance, côté serveur** — aucun contrôle de comptage dans `UserController.CreateUser`, pas de 422. ⚠️ **Ce que manager-app vient de livrer est un garde-fou d'interface, pas une règle** : un `POST /api/User` direct sur l'API passe toujours, et rien n'empêche une instance d'avoir 12 utilisateurs. Le plafond n'est pas non plus dans `SubscriptionPlan`, qui ne porte que 5 champs (stockage, jetons IA, stats, rétention, stats avancées) — s'il devait varier par plan, c'est une colonne à ajouter, donc un changement de schéma **après** le gel du lot B
|
||||
- ⚠️ **Retour d'erreur du POST, corrigé côté front dans la même passe** : `invokeAPI` ne lève pas sur un code d'erreur et son résultat était ignoré — un e-mail déjà utilisé (409) ou un rôle refusé (403) laissait la liste inchangée **sans aucun message**. Le statut est désormais testé et le corps affiché. C'est ce qui rendra le futur 422 visible sans retouche front
|
||||
|
||||
#### Comportement attendu (spec d'origine)
|
||||
- Un `InstanceAdmin` peut voir, ajouter et supprimer des utilisateurs **de son instance uniquement**
|
||||
@ -516,13 +517,18 @@
|
||||
- `SaveChanges()` (sync) et `SaveChangesAsync()` déclenchent tous les deux l'audit (avant : seul `SaveChangesAsync` l'alimentait, les controllers en sync passaient sous le radar)
|
||||
- `DateCreation`/`DateUpdate` auto-remplis via `IAuditableEntity` + `ChangeTracker`, plus besoin de le faire à la main dans chaque mapper
|
||||
|
||||
- [ ] ❌ **Manager-app — écran "Activité / Audit" (SuperAdmin uniquement)**
|
||||
- Nouveau modèle + service dans `manager_api_new/` en lien avec `AuditController` (`GET /api/Audit`) — rien n'existe encore côté client généré, à ajouter à la main (pas de régénération OpenAPI, cf. politique du projet)
|
||||
- Écran liste : date, instance (nom, pas juste l'ID), type d'entité, action (Create/Update/Delete), utilisateur — tri par date décroissante
|
||||
- Filtres : par instance (picker), par type d'entité, par utilisateur, par plage de dates
|
||||
- Tap sur une ligne → détail avant/après (`OldValues`/`NewValues` formatés, pas le JSON brut)
|
||||
- **Usage cible** : le SuperAdmin (Thomas) veut pouvoir répondre à "qu'est-ce que ce client a fait durant telle période ?" sans aller chercher en base
|
||||
- Accès : nouvelle entrée de menu visible uniquement si `role == SuperAdmin`
|
||||
- [x] ✅ **Manager-app — écran "Activité / Audit" (SuperAdmin uniquement)** — livré le 2026-08-12
|
||||
- `Screens/Audit/audit_screen.dart` + `audit_entry.dart`, entrée de menu `menuId: 13` conditionnée à `role.value == 0` (même règle que la policy `SuperAdmin` de l'endpoint)
|
||||
- Liste triée par date décroissante, filtres instance / type d'entité / utilisateur / plage de dates, pagination 50 par page, clic sur une ligne → détail avant/après en table champ / avant / après
|
||||
- **`manager_api_new` n'a pas été touché.** Le plan initial disait « nouveau modèle + service dans `manager_api_new/` » ; l'écran passe par un `http.get` direct avec le Bearer, comme `_loadKnowledge` / `_loadInsights` / `_reindex` du Guide IA. Une lecture seule ne justifie pas d'étendre un client qui s'édite à la main — et le déclencheur de génération (`openApiTest.dart`) n'existe plus depuis le lot A
|
||||
- Deux détails qui ne se devinent pas : le contrôleur renvoie les entités **brutes** (pas de DTO), donc les champs sont ceux d'`AuditLog` en camelCase ; et `to` est pris à **la fin de la journée** choisie, sinon un filtre « jusqu'au 12/08 » exclut tout le 12/08
|
||||
- Les valeurs multilingues (`List<TranslationDTO>`) sont rendues en `FR : …` par ligne plutôt qu'en JSON brut — c'est le gros des colonnes modifiées sur une section ou une configuration
|
||||
|
||||
- [ ] ❌ **Backend — aucune section n'est jamais journalisée** *(découvert le 2026-08-12 en câblant l'écran)*
|
||||
- `AuditedTypes.Contains(entry.Entity.GetType())` (`MyInfoMateDbContext.cs:126`) exige une **égalité de type exacte**. Or `Section` est **abstraite** (`Section.cs:17`) : le type runtime est toujours `SectionMap`, `SectionQuiz`, `SectionArticle`… La ligne `typeof(Section)` d'`AuditedTypes` ne matche donc **rien**
|
||||
- Conséquence : `Resource`, `Configuration`, `Device`, `User`, `Instance` sont bien journalisés, **le contenu ne l'est pas** — exactement ce que l'usage cible voulait tracer (« qu'est-ce que ce client a fait durant telle période ? »)
|
||||
- Correctif attendu : tester `AuditedTypes.Any(t => t.IsInstanceOfType(entry.Entity))` au lieu de `Contains`. Vérifier au passage que `EntityType` porte alors `SectionMap` et non `Section` — l'écran affiche la valeur brute pour tout type qu'il ne connaît pas, donc il fonctionnera sans retouche, mais le filtre « Section » ne matchera plus rien : il faudra soit normaliser côté serveur, soit élargir la liste du front aux 13 sous-types
|
||||
- Le filtre « Section » **est déjà dans l'écran** : le jour où le backend corrige, le front n'a rien à changer. D'ici là ce filtre ne rend aucune ligne
|
||||
|
||||
---
|
||||
|
||||
@ -754,9 +760,9 @@
|
||||
|---|---|---|---|---|
|
||||
| Mode offline — réécriture complète | ❌ | N/A | N/A | ❌ |
|
||||
| Audit log API Keys | ❌ | N/A | N/A | N/A |
|
||||
| Audit log — écran consultation SuperAdmin | ✅ | ❌ | N/A | N/A |
|
||||
| Audit log — écran consultation SuperAdmin | ⚠️ ✅ sauf les sections, **jamais journalisées** (2026-08-12) | ✅ 2026-08-12 | N/A | N/A |
|
||||
| Rate limiting API Keys | ❌ | N/A | N/A | N/A |
|
||||
| Gestion users par instance admin (max 5, pas de SuperAdmin) | ⚠️ scoping ✅, **plafond 5 ❌** (révisé 2026-08-05) | ⚠️ compteur "X / 5" ❌ | N/A | N/A |
|
||||
| Gestion users par instance admin (max 5, pas de SuperAdmin) | ⚠️ scoping ✅, **plafond 5 ❌** (revérifié 2026-08-12) | ✅ compteur « X / 5 » + bouton désactivé (2026-08-12) — garde-fou d'interface seulement | N/A | N/A |
|
||||
| **Infra** — Port PostgreSQL fermé (accès via tunnel SSH uniquement) | ❌ | N/A | N/A | N/A |
|
||||
| **GuidedPath** — Image de couverture (ImageResourceId backend) | ✅ | ✅ (révisé 2026-08-05) | N/A | ✅ via `imageUrl` |
|
||||
| **GuidedPath/SectionParcours** — Flag ShowMap backend | ✅ (sur SectionParcours) | ✅ `section_parcours_config.dart:63` (révisé 2026-08-05) | N/A | ✅ `ParcoursSection.tsx:101` |
|
||||
|
||||
73
v1-plan.md
73
v1-plan.md
@ -35,6 +35,8 @@ C'est la partie qui manque partout ailleurs. Neuf liens réels, dont quatre impo
|
||||
| L3 | **`pg_dump` → bascule (point 18)** *et* **→ activation de `visit-events-purge`** | Un seul item débloque deux choses : le filet du jour J, et le job de purge des stats volontairement inactif tant que `Stats:RetentionDays` n'est pas défini |
|
||||
| L4 | **RGPD → mise en service de `VisitorQuestion`** — **repoussé en fin de backlog le 2026-08-11 (lot J)** | Ce lien reste vrai, mais il ne contraint plus l'ordre : **rien ne part en prod avant que tout le backlog soit terminé** (§1quater de STATUS.md). Le lot J étant le dernier avant la bascule, le volet RGPD est en place avant la première question de visiteur réelle — la journalisation ne tourne d'ici là que sur des bases de développement. **La condition tient tant que cette porte tient** : si un déploiement anticipé était décidé, le lot J redeviendrait bloquant |
|
||||
|
||||
| L19 | **Portage des apps visiteur (lot K) → bascule** — **ajouté le 2026-08-12** | Le contrat d'API a changé avec Postgres v3 : `ConfigurationDTO.roundedValue`, `GeoPointDTO.latitude/longitude` ne sont plus servis. **Les apps déployées parlent l'ancien contrat.** Basculer sans avoir porté et publié les apps, c'est dégrader les clients existants le jour J — sans erreur visible, juste des cartes vides. ⚠️ **Le parc des téléphones visiteurs ne se force pas** : c'est ce qui impose la stratégie de coexistence du lot I plutôt qu'une bascule sèche |
|
||||
|
||||
### Liens d'économie — même code, une seule passe
|
||||
|
||||
| # | Lien | Ce qu'on paie deux fois sinon |
|
||||
@ -74,7 +76,7 @@ Rien de créatif, tout est un verrou pour la suite.
|
||||
| ~~Committer le lot 3 RAG du 10/08~~ | — | ✅ **Fait le 2026-08-11.** Les **9 repos** sont propres et synchronisés avec leur remote. `manager-service` `18e4240`, `manager-app` `eb8e849`. ⚠️ **`DOCS/` est lui-même un repo git** (branche `main`) — facile à oublier quand on énumère les repos de code, et c'est là que vivent ce plan, STATUS.md et le kanban |
|
||||
| ~~**Rotation des secrets**~~ | `manager-service` | 📅 **Déplacée au lot I (I0)** le 2026-08-11 — repos privés sur Gitea auto-hébergé. **L1 tient : elle précède toujours I3.** Voir la note de L1 pour les conditions qui la font remonter |
|
||||
| Nettoyer `manager_api_new` : supprimer les 14 fichiers modèle orphelins + `lib/api/openApiTest.dart` | `manager-app` | **L8** — rend `flutter analyze` exploitable pour les lots D, E, F. **Diagnostic fermé le 2026-08-11**, détail ci-dessous |
|
||||
| Réparer le build `tablet-app` | `tablet-app` | Porte du test-plan §0. **Un seul défaut, purement Gradle** (`flutter_tools/gradle/build.gradle.kts`) — peut-être déjà réglé par `5a6701d`, à reconfirmer par un vrai build. Une piste Dart a été ouverte puis **écartée** le 2026-08-11 : `marker_view.dart:456` référence `ContentGeoPoint`, classe hors graphe, mais **la ligne est dans un bloc commenté** (`/*` ouvert en 418), comme sa jumelle de `mymuseum-visitapp:367` (`/*` en 329). `flutter analyze` sur les deux fichiers : zéro erreur. Ne pas rouvrir |
|
||||
| ~~Réparer le build `tablet-app`~~ | `tablet-app` | ➡️ **Déplacé au lot K le 2026-08-12. Le diagnostic « un seul défaut, purement Gradle » était doublement faux** — il a tenu tant que personne n'avait lancé un vrai build. Ce n'est pas un correctif de lot A, c'est un chantier de rattrapage. Voir le lot K |
|
||||
| Sécurité rapide : `EnableSensitiveDataLogging` coupé en prod (`Startup.cs:248`), backdoor `#if DEBUG` retirée, exceptions internes plus exposées | `manager-service` | À faire avant que la prod existe, pas après |
|
||||
|
||||
**Pourquoi le nettoyage de `manager_api_new` est sans risque de build** — vérifié le 2026-08-11, à ne pas re-douter :
|
||||
@ -118,7 +120,7 @@ L'étape A du §1quater est faite (`StoragePath`/`SizeBytes` écrits à `Create`
|
||||
| C2 | Backfill : `StoragePath` par `UPDATE` SQL (chemin déterministe `pictures/{instanceId}/{resourceId}`), `SizeBytes` par listing du bucket Firebase | **37 lignes sur 45.** Les 7 types URL (`ImageUrl`/`VideoUrl`/`JSONUrl`) n'ont pas de blob, la 8ᵉ est un type fichier sans URL — inventaire des orphelins au passage |
|
||||
| C3 | Quota de stockage autoritaire : pré-vol + contrôle à `Create` + suppression du blob à `Delete` | **L10** — après C2, sinon le contrôle porte sur des zéros |
|
||||
| C4 | Compression images côté client, 2560 px / JPEG q82 (~12×) | `manager-app`. Indépendant de C2 : ne touche que les nouveaux uploads |
|
||||
| C5 | Alerte de budget GCP | 5 minutes. À faire dès maintenant, pas au jour J |
|
||||
| ~~C5~~ ✅ **2026-08-12** | Alerte de budget GCP **posée dans la console** | Fait côté GCP, rien à vérifier dans le code |
|
||||
|
||||
### Lot D — Bugs offline (1 à 2 jours, après mesure)
|
||||
|
||||
@ -177,7 +179,9 @@ Ne change pas : la popup de traduction (un niveau justifié, langues verticales,
|
||||
| Aperçu de conversation | **L9** — converger avec « Mode preview / cible Assistant-Persona » |
|
||||
| Onglet « Vocal » dans les stats | |
|
||||
| `GetSummary` agrégé en SQL au lieu de `eventsQuery.ToList()` | **L13** — devenu pressant par la rétention 13 mois |
|
||||
| Écran d'audit log (backend fait, front absent) + gestion des users par instance (plafond de 5, compteur UI) | **L8** |
|
||||
| ~~Écran d'audit log~~ · ~~gestion des users par instance (compteur UI)~~ | 🔨 **Front livré le 2026-08-12**, `manager-app` seul. Écran `Screens/Audit/` sur `GET /api/Audit` (filtres instance / type / utilisateur / dates, pagination 50, détail avant-après), entrée de menu `menuId: 13` réservée à `role.value == 0`. Compteur « X / 5 utilisateurs » et bouton d'ajout désactivé au plafond, SuperAdmin exclu (sa liste couvre toutes les instances). 40 clés i18n FR/EN/NL, `flutter build web` ✅. **`manager_api_new` intact** : `http.get` direct au Bearer, comme le Guide IA — une lecture seule ne justifie pas d'étendre un client qui s'édite à la main.<br>⚠️ **Deux dettes backend ouvertes par ce chantier, volontairement non traitées** (voir les deux lignes suivantes) | **L8** |
|
||||
| **Plafond de 5 utilisateurs côté serveur** | ❌ `UserController.CreateUser` ne compte rien, pas de 422. Ce qui est livré côté front est un **garde-fou d'interface** : un POST direct sur l'API passe toujours. Le plafond n'est pas non plus dans `SubscriptionPlan` (5 champs, aucun sur les utilisateurs) — s'il devait varier par plan, c'est une colonne, donc un changement de schéma **après** le gel du lot B. Le retour d'erreur du POST est déjà branché côté front : le 422 sera visible sans retouche |
|
||||
| **Aucune section n'est journalisée** | ❌ `AuditedTypes.Contains(entry.Entity.GetType())` (`MyInfoMateDbContext.cs:126`) exige l'égalité exacte, or `Section` est **abstraite** (`Section.cs:17`) : le type runtime est toujours `SectionMap`, `SectionQuiz`… donc `typeof(Section)` ne matche jamais. Le journal couvre `Resource`/`Configuration`/`Device`/`User`/`Instance` mais **pas le contenu** — précisément ce que l'écran devait tracer. Correctif : `Any(t => t.IsInstanceOfType(...))`. ⚠️ `EntityType` portera alors `SectionMap` et non `Section` : soit normaliser côté serveur, soit élargir la liste du filtre front aux 13 sous-types |
|
||||
| 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 |
|
||||
| Rate limiting sur les API Keys, Stripe Customer Portal | §1 « À faire » |
|
||||
| Tests du vector store via `Testcontainers.PostgreSql` sur `Dockerfile.postgres` | **L14** — au minimum, éprouver le post-filtrage HNSW **à deux instances** avant la bascule |
|
||||
@ -242,6 +246,35 @@ Ordre du §2 de STATUS.md, corrigé par **L12**.
|
||||
| H4 | **§18 — onboarding self-service** | **L12** — après le déploiement web du lot I. Prérequis de config : alias `onboarding@myinfomate.be` et `whsec_` de la Stripe CLI |
|
||||
| H5 | §1-17 par lot, §16 non-régression | |
|
||||
|
||||
### Lot K — Apps visiteur à niveau pour la bascule (3 à 5 jours) — **ajouté le 2026-08-12**
|
||||
|
||||
> **Ce lot n'existait pas, et il est bloquant.** Il est né d'un vrai build de `tablet-app`, le premier depuis des mois : le diagnostic des docs (« un seul défaut, purement Gradle, peut-être déjà réglé par `5a6701d` ») ne tenait que parce que personne n'avait lancé la commande.
|
||||
>
|
||||
> ⚠️ **Le point qui fait de ce lot un bloquant de la bascule, et que le plan ne portait nulle part : le contrat d'API a changé, les apps déployées parlent l'ancien.** `ConfigurationDTO.roundedValue` et `GeoPointDTO.latitude/longitude` ne sont plus servis par Postgres v3. Une tablette en service chez MDLF ou au Fort ne s'arrêterait pas proprement le jour J — elle afficherait des cartes sans points et des écrans dégradés. Voir **L19**.
|
||||
|
||||
**K1 — ✅ Outillage Android `tablet-app`, fait le 2026-08-12.** Six crans, chacun dicté par l'erreur du précédent — à ne pas re-découvrir un par un :
|
||||
|
||||
| # | Changement | Ce qu'il débloquait |
|
||||
|---|---|---|
|
||||
| 1 | Gradle **7.5 → 8.11.1** | le plugin Kotlin exigeait ≥ 7.6.3 |
|
||||
| 2 | AGP **7.2.0 → 8.5.0**, Kotlin **1.9.0 → 2.0.10** | `onVariants$default` : Kotlin 1.9 appelle une API absente d'AGP 7.2 |
|
||||
| 3 | `android.bundle.enableUncompressedNativeLibs` **retirée** | option supprimée en AGP 8.1. Valait `false`, le défaut est `true` : les `.so` ne sont plus compressés dans l'AAB |
|
||||
| 4 | AGP → **8.7.3** | minimum exigé par Flutter (8.6.0) |
|
||||
| 5 | AGP → **8.9.1** | réclamé par `androidx.browser:1.9.0` et `androidx.core:core-ktx:1.17.0`, tirés par les plugins |
|
||||
| 6 | `jcenter()` → `mavenCentral()` | jcenter est arrêté depuis 2021 |
|
||||
|
||||
⚠️ **Un bloc `buildscript` entièrement commenté dans `android/build.gradle`** déclarait AGP 8.5.0 / Kotlin 2.0.10 alors que la config appliquée vivait dans `settings.gradle` (AGP 7.2.0 / Kotlin 1.9.0). Il a fait conclure deux fois à une configuration qui n'était pas celle du build — **il est supprimé**, les versions ne sont plus déclarées qu'à un seul endroit. C'est le troisième piège « bloc commenté » du projet, après les deux pistes Dart écartées le 11/08.
|
||||
|
||||
⚠️ **Dette d'outillage restante, non bloquante, à traiter en V2** : Flutter annonce l'abandon de Kotlin < 2.2.20, et **8 plugins appliquent le Kotlin Gradle Plugin** (`mapbox_maps_flutter`, `video_player_android`, `webview_flutter_android`, `device_info_plus`, `fluttertoast`, `package_info_plus`, `shared_preferences_android`, `wakelock_plus`) — les futures versions de Flutter refuseront de construire dans ce cas. **`compileSdkVersion 36` n'est pas négociable** : sans lui, plus de mise à jour publiable sur le store.
|
||||
|
||||
| # | Quoi | Note |
|
||||
|---|---|---|
|
||||
| **K2** | **Portage du contrat d'API** — **132 erreurs Dart, 3 causes** : `ConfigurationDTO.roundedValue`/`isDate`/`isHour`/`isSectionImageBackground`/`screenPercentageSectionsMainPage` ont migré vers **`AppConfigurationLinkDTO`** (73 err.) · `GeoPointDTO.latitude/longitude` → **`GeometryDTO? geometry`**, effet de PostGIS (40 err.) · les `min()` sur `dynamic` en découlent (19 err.) | **Le modèle est déjà écrit** : `mymuseum-visitapp` a fait ce portage (`visitAppContext.currentAppConfigurationLink?.roundedValue`, `Models/visitContext.dart`). Copier, ne pas concevoir. Les 40 sites `latitude`/`longitude` se traitent par **une extension Dart sur `GeoPointDTO`** plutôt qu'un à un |
|
||||
| **K3** | **Périmètre kiosk — tranché le 2026-08-12.** `tablet-app` couvre **11 des 13** types. **`SectionParcours` : exclu définitivement** — un parcours guidé fait marcher le visiteur avec géodéclenchement, ça n'a aucun sens sur borne fixe. **`SectionEvent` : à supporter** — une borne d'accueil qui affiche le programme du jour et le plan est le cas d'usage kiosk par excellence, et c'est le scénario du §19.13 cas E | Ni l'un ni l'autre n'est un retard : les deux types **sont nés avec Postgres v3**, ils n'ont jamais existé dans Mongo (cf. lot G, écart a) |
|
||||
| **K4** | **`SectionPuzzle` → `SectionGame` + le type glissant.** Le serveur a renommé et porte `GameTypes { Puzzle, SlidingPuzzle }` ; `tablet-app` a encore un écran `Puzzle/` qui ne connaît que l'ancien. **Reprendre le code de `mymuseum-visitapp/lib/Screens/Sections/Game/`** — `game_page.dart` et `sliding_puzzle_piece.dart` existent déjà là-bas, et cette version est **moins buguée** que celle de tablet-app | Récupération, pas réécriture |
|
||||
| **K5** | **`mymuseum-visitapp` : même rattrapage d'outillage.** Son APK ne compile pas non plus (Gradle/NDK) — c'est ce qui empêche de vérifier D1 sur device et **bloque D0 / test-plan §21** | Son code est déjà porté sur le nouveau contrat ; c'est l'outillage qui manque, pas le portage |
|
||||
| **K6** | **Publier les APK à jour**, puis vérifier le renouvellement du parc | **L19**. Les tablettes kiosk sont des appareils que tu gères — mise à jour maîtrisée. **Les téléphones des visiteurs, non** : aucune manœuvre technique ne force une mise à jour, d'où la stratégie de coexistence du lot I |
|
||||
|
||||
### Lot J — RGPD & conformité (2 à 3 jours) — juste avant le lot I
|
||||
|
||||
> **Placé en fin de backlog le 2026-08-11**, à la demande. C'est tenable pour une seule raison : la porte « rien en prod avant la fin du backlog ». La journalisation des questions ne tourne d'ici là que sur des bases de développement, donc aucun visiteur réel n'est concerné. **Si cette porte saute, ce lot remonte devant.**
|
||||
@ -264,7 +297,7 @@ Les CGU §8 ont été réécrites le 2026-08-11 (`cgu-myinfomate.md`) et le text
|
||||
|
||||
| # | Quoi | Lien |
|
||||
|---|---|---|
|
||||
| **I0** | **Rotation des secrets** : JWT signing key, clés Gemini/OpenWeather, connection strings, mot de passe MQTT, token Telegram → variables d'environnement, purge de l'historique Git, suppression de `RELEASE/` | **L1** — déplacée du lot A le 2026-08-11. **Doit précéder I3** : la prod ne doit pas naître avec les clés de l'historique, sinon il faut rotationner deux fois. C'est le premier geste du lot I, pas une option |
|
||||
| **I0** | **Rotation des secrets** : JWT signing key, clés Gemini/OpenWeather, connection strings, mot de passe MQTT, token Telegram → variables d'environnement, purge de l'historique Git, suppression de `RELEASE/`.<br>⚠️ **Inventaire élargi le 2026-08-12 : les apps Flutter en portent aussi.** Trouvé en réparant le build tablet-app : `SDK_REGISTRY_TOKEN=sk.…` — un token **MapBox secret** (préfixe `sk.`, pas `pk.`), au nom du compte perso, en clair dans `tablet-app/android/gradle.properties`, versionné. Il sert à télécharger le SDK MapBox au build, donc **le retirer casse le build** tant qu'il n'est pas passé en variable d'environnement — à faire dans la même passe, pas avant. **Passer les 3 repos Flutter au peigne** : l'inventaire d'origine ne couvrait que `manager-service`, on aurait rotationné la moitié des secrets en croyant le sujet clos | **L1** — déplacée du lot A le 2026-08-11. **Doit précéder I3** : la prod ne doit pas naître avec les clés de l'historique, sinon il faut rotationner deux fois. C'est le premier geste du lot I, pas une option |
|
||||
| I1 | **`pg_dump` quotidien** en cron, en complément du snapshot OVH | **L3** — puis activer `Stats__RetentionDays=395` et vérifier que `visit-events-purge` ne supprime rien |
|
||||
| I2 | Backup Mongo automatique en cron (dump + `docker cp` vers l'hôte) | Filet du rollback |
|
||||
| I3 | **Un seul chantier compose/Traefik** : `Dockerfile.postgres` épinglé par digest, **port 5432 fermé**, entrée `app.myinfomate.be` + Dockerfile `visitapp-web` | **L7** — trois cartes, un fichier. **L1** : après la rotation des secrets |
|
||||
@ -307,7 +340,27 @@ 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 | **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 | |
|
||||
| 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** |
|
||||
|
||||
**Stratégie de bascule — coexistence sur deux serveurs (décidée le 2026-08-12).**
|
||||
|
||||
Plutôt qu'une bascule sèche avec fenêtre de maintenance : **un second serveur** porte la nouvelle prod Postgres, les **nouvelles versions des apps** pointent vers lui, et l'ancienne prod Mongo continue de servir les apps déjà installées. Pas de bascule DNS, pas de fenêtre, et un retour arrière qui consiste à ne rien faire.
|
||||
|
||||
C'est la bonne réponse au problème que **L19** pose et qu'aucune manœuvre technique ne résout : on ne force pas la mise à jour du téléphone d'un visiteur.
|
||||
|
||||
**Ce qui rend la coexistence saine — une seule source d'écriture :**
|
||||
|
||||
Le contenu n'est écrit que par le back-office. Dès que `manager-app` pointe sur la nouvelle prod, **l'ancienne devient figée**, en lecture seule de fait. Les vieilles apps voient un contenu gelé au jour J, ce qui est acceptable quelques semaines — et surtout, **les deux bases ne divergent jamais**. Si les deux restaient écrivables, il n'y aurait plus de source de vérité au bout d'une journée.
|
||||
|
||||
⚠️ **Ce qui ne marche pas : « recopier de la nouvelle prod vers l'ancienne ».** Il n'existe pas de chemin Postgres → Mongo. `MigrationController` va dans un seul sens, et écrire l'inverse coûterait le même travail — pour alimenter une base qu'on éteint. **L'ancienne prod se fige, elle ne se synchronise pas.** Si un contenu doit absolument arriver sur les vieilles apps, il se saisit à la main des deux côtés, ou on attend le renouvellement du parc.
|
||||
|
||||
**Conséquences à tenir :**
|
||||
|
||||
- **Le domaine** : les anciennes apps gardent `api.mymuseum.be`, les nouvelles reçoivent une nouvelle adresse. Aucun DNS ne bouge — c'est ce qui rend le rollback trivial.
|
||||
- **Coût** : un second serveur OVH le temps de la transition. À arbitrer contre le risque d'une bascule sèche ; à ce stade du projet, ça les vaut.
|
||||
- **Extinction** : décider d'une date de fin de l'ancienne prod, sinon elle survit trois ans. Critère proposé : quand le contenu gelé devient gênant pour un client, ou quand le parc s'est renouvelé.
|
||||
- **Les tablettes kiosk ne sont pas concernées par l'attente** : ce sont des appareils gérés, mis à jour directement — elles passent sur la nouvelle prod le jour même.
|
||||
- **I5 (plan de retour arrière) est à réécrire** : avec deux serveurs, le rollback n'est plus une restauration de dump mais « republier l'app qui pointe vers l'ancienne adresse ». Plus simple, mais tributaire des délais de validation des stores — ce qui devient le vrai risque à couvrir.
|
||||
|
||||
### Après la prod
|
||||
|
||||
@ -321,8 +374,9 @@ Reprendre contact avec **Louise Smets — Musée d'Ixelles**, une fois la prod s
|
||||
A (½-1j) ──► B (2-3j) ──► G (3-4j) ──► H (1 sem.) ──► I (2-3j + fenêtre)
|
||||
│ │
|
||||
│ └──► C1 ──► C2 ──► C3
|
||||
│ C4, C5 (indépendants)
|
||||
├──► D0 (test device) ──► D1..D5
|
||||
│ C4 (C5 ✅)
|
||||
├──► K1 ✅ ──► K5 ──► D0 (test device) ──► D1..D5
|
||||
│ └──► K2 ──► K3, K4 ──► K6 (publier) ──► bascule (L19)
|
||||
├──► D-bis : DB0 ──► DB1 ──┬──► DB2
|
||||
│ ├──► DB3 ──► F (job de thèmes)
|
||||
│ └──► DB4 ✅ (A tranchée et livrée)
|
||||
@ -333,10 +387,13 @@ A (½-1j) ──► B (2-3j) ──► G (3-4j) ──► H (1 sem.) ──► I
|
||||
```
|
||||
|
||||
- **Chemin critique : A → B → G → H → I.** Tout le reste s'y greffe.
|
||||
- **Le lot K s'y greffe par la fin** : K2/K3/K4 sont parallélisables avec C, D, E et F, mais **K6 (publier les apps) précède la bascule** (**L19**). Et **K5 précède D0** : sans APK `mymuseum-visitapp`, pas de test §21 sur device, donc pas de mesure des bugs offline restants.
|
||||
- **C, D, D-bis, E, F sont parallélisables** entre eux, mais **C1 doit précéder G** (L5) et **B doit précéder G** (L2).
|
||||
- **DB3 doit précéder le job de thèmes du lot F** (L16) : c'est le seul endroit du plan où le design commande le backend.
|
||||
- **La boucle du graphe a disparu le 2026-08-11.** Elle venait de : I3 dépend de la rotation des secrets (dans le lot A), et H4 dépend de I3 (L12). En déplaçant la rotation en **I0**, la contrainte devient linéaire — `I0 → I3 → H4` — et il n'y a plus de cycle à contourner. **Contrepartie à ne pas rater** : H4 (test d'onboarding) exige `app.myinfomate.be` déployé, donc I3, donc I0. Concrètement, **la rotation des secrets doit être faite avant la fin du lot H**, pas au tout dernier moment. C'est le seul endroit où le report du 11/08 crée une échéance interne.
|
||||
|
||||
**⚠️ Révision du 2026-08-12 : +3 à 5 jours pour le lot K**, découvert en lançant le premier vrai build de `tablet-app`. Ce n'est pas du périmètre ajouté, c'est du travail qui était déjà dû et que personne n'avait vu : sans lui, la bascule dégrade les clients existants.
|
||||
|
||||
**Ordre de grandeur : 5 à 6 semaines**, dont ~1 semaine de tests, 3-4 jours sur `MigrationController` seul, 1,5 à 2 semaines sur le lot design et 2-3 jours sur le lot J (RGPD). **Les trois arbitrages structurants sont tranchés depuis le 2026-08-11** — `ParcoursIds` et `QuestionType` au lot B, le flux SectionParcours au lot D-bis : le plan n'attend plus de décision, seulement du développement.
|
||||
|
||||
---
|
||||
@ -381,4 +438,6 @@ Les plans détaillés de `v2/` ne se lisent qu'au moment d'attaquer le lot conce
|
||||
|
||||
Reporté en V2 et confirmé ici : SectionForm, ressource 360°, AR image tracking (Mind AR), kiosk web, ingestion documentaire + OCR, TTS pré-généré, guides multiples et thématiques, talking head, génération de visites, envoi mensuel automatique du rapport de stats, XR (Ray-Ban / Quest), dashboard super-admin V2, facturation V2.
|
||||
|
||||
**Ajouté le 2026-08-12 : l'écran SuperAdmin d'activation de l'add-on IA et des canaux d'une instance.** Il rendrait l'add-on vendable en libre-service, mais la V1 n'en a pas besoin : les 4 clients de la bascule sont affectés à leur plan (§lot I, point 3), et doter une instance à la main reste un geste interne, fait une fois, sur une base qu'on contrôle. Le mode d'emploi SQL est dans **STATUS.md §1sexies « Add-on IA »** — trois gestes, dont le bouton de relance d'indexation qu'un `UPDATE` seul ne déclenche pas. À faire d'un bloc avec l'endpoint de mise à jour d'`ApplicationInstance` (canaux activables), qui reste lui au lot F : c'est le même écran, mais seule la partie canaux est V1.
|
||||
|
||||
Le POC Ray-Ban existe et est démontrable (branche `Meta-Rayban-Test`, jamais mergée) — c'est un actif commercial, pas un chantier V1. Ne pas vendre l'add-on avant la bascule TTS ElevenLabs → Gemini.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user