From ff80d9e6d2fdd8e02291c93aff8c6f3279628673 Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Wed, 12 Aug 2026 12:05:47 +0200 Subject: [PATCH] =?UTF-8?q?Docs=20:=20lot=20m=C3=A9dias=20termin=C3=A9=20c?= =?UTF-8?q?=C3=B4t=C3=A9=20serveur=20(C2,=20C3)=20et=20K2=20livr=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lot C — C2 et C3 passent en livré dans v1-plan.md et STATUS.md §1sexies, avec ce que la doc annonçait de faux et qu'il ne faut pas re-croire : - La méthode de C2, « SizeBytes par listing du bucket Firebase », était inapplicable — le serveur n'avait aucun client de stockage. Le sondage passe par HEAD, via un sondeur partagé avec la migration. - Le pré-vol de C3 existait déjà à moitié sans être documenté : le chemin multipart contrôlait, le chemin JSON non, et les deux lectures du quota divergeaient entre le plan et l'instance. - L'angle mort laissé par C1 était une fausse crainte : le type n'entre pas dans le chemin de stockage. - Le « 37 lignes sur 45 » disparaît au profit de l'inventaire que le backfill produit lui-même. Deux suites que ces chantiers créent, consignées pour ne pas se perdre : - C6, nouvelle ligne du lot C : manager-app supprime toujours le blob de son côté, après la ligne et en avalant l'échec. Depuis C3 c'est du code mort. À retirer, mais pas avant que Firebase:StorageBucket soit posé en prod, sinon plus personne ne supprime. - I9, nouvelle étape du lot I : renseigner Firebase:StorageBucket, puis lancer le backfill en dryRun avant de l'appliquer. Sans lui le quota porte sur des SizeBytes à zéro pour tout l'existant et laisse tout passer (L10). S'y ajoute un piège de build : dotnet restore échoue en 401 si la source NuGet git.dev-espaces-naturels.lu est déclarée dans l'image, et Google.Cloud.Storage.V1 est le premier paquet ajouté depuis longtemps. Lot K — K2 livré : tablet-app est rebranché sur le contrat d'API de Postgres v3, les 132 erreurs de contrat sont tombées, et la table des conventions de coordonnées est consignée. Reste K4, le renommage Puzzle → Game. Point de vigilance noté : le filtre des configurations tablette a changé de source, à valider sur device. Compteurs du kanban recomptés colonne par colonne : Urgent 4, Migration v3 2, Bugs ouverts 5, À tester 6, Planifié 20, Bascule prod 8, Fait récemment 38. Co-Authored-By: Claude Opus 5 --- STATUS.md | 43 ++++++++++++++++++++++++++++++++++++++ kanban.html | 59 +++++++++++++++++++++++++++++++---------------------- v1-plan.md | 9 +++++--- 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/STATUS.md b/STATUS.md index 92dde8d..35b3aa6 100644 --- a/STATUS.md +++ b/STATUS.md @@ -326,6 +326,30 @@ Les lots 1 et 2 sont terminés. **Le lot 3 et le lot médias sont tous les deux **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. +**✅ K2 livré le 2026-08-12 — les 132 erreurs de contrat sont tombées.** `flutter analyze lib` ne renvoie plus que 6 erreurs `PuzzleDTO`, qui relèvent de K4. Livré : `TabletAppContext.currentAppConfigurationLink`, `lib/Helpers/geo.dart` (extension `lat`/`lng` + `coordinateKey`), 67 accès `roundedValue` rebranchés, `main_view` et les 4 vues carte portées. + +**Deux bugs latents trouvés au passage — la raison pour laquelle un chercher-remplacer aurait été dangereux :** + +- ⚠️ **`applicationInstanceDTO` servait de drapeau d'assistant.** Il n'était assigné **que si** `instanceDTO.isAssistant == true` (`main_view:339`). Ce DTO portant désormais les `AppConfigurationLink`, le nouveau getter aurait renvoyé `null` sur **toute instance sans IA — donc MDLF et le Fort Saint-Héribert** — et `roundedValue`, `isDate`, `isHour`, `isSectionImageBackground`, `screenPercentageSectionsMainPage` seraient tous retombés sur leurs valeurs par défaut. **Sans erreur, sans log** : des écrans qui ne ressemblent plus à ce qui est configuré. Corrigé par une assignation inconditionnelle et un drapeau explicite `isAssistantEnabled`, qui **préserve le ET** instance × canal — les deux gardes qu'on retrouve côté serveur en `AiController:315` et `:360`. +- ⚠️ **Le filtre « configurations pour tablette » n'avait plus de source.** `ConfigurationDTO.isTablet` a disparu : le canal est porté par l'`ApplicationInstance` et le rattachement vit dans ses `AppConfigurationLink`. `getConfigurations` filtre désormais là-dessus. **À valider sur device (§0/§18)** : si un client a des configurations non rattachées à son canal tablette, l'écran de sélection sera **vide**. L'ancien code avait le même angle mort, mais la donnée qui pilote le filtre a changé — donc à revérifier sur les 4 instances. + +⚠️ **Troisième piège de vérification de la journée** : `flutter analyze` écrit `error - `, pas `error • `. Un grep sur la mauvaise forme a fait conclure « 0 erreur » alors qu'il en restait 10. Après le `| tail` qui masque le code de sortie, c'est le deuxième faux vert obtenu par un filtre mal formé — **toujours compter les erreurs en relisant le log complet**. + +#### ⚠️ Deux conventions de coordonnées coexistent — vérifié le 2026-08-12, ne pas « harmoniser » à l'aveugle + +Le projet écrit les coordonnées **dans deux ordres différents selon le producteur**. Les deux sont en production, et **une confusion ne lève aucune erreur** : elle place les points ailleurs, sur une carte qui reste plausible. + +| Donnée | Ordre | Écrit par | Lu par | +|---|---|---|---| +| **`GeoPointDTO.geometry`**, et toute géométrie passée par `GeometryMapper` | **`[lng, lat]`** — GeoJSON standard | `GeometryMapper.ToDto` sérialise `{ point.X, point.Y }` ; `MigrationController:798` construit `new Point(lon, lat)` | `mymuseum-visitapp/Models/agenda.dart:72` lit bien `coords[1]`=lat, `coords[0]`=lng · `tablet-app/lib/Helpers/geo.dart` (créé le 12/08) | +| **`GuidedStep.geometry`** | **`[lat, lng]`** — inversé | `manager-app`, `map_geometry_picker` | `visitapp-web/src/lib/geo.ts`, qui le documente explicitement | + +**Pourquoi on ne corrige pas** : l'ordre inversé des GuidedStep est déjà écrit en base. Le redresser demanderait une migration de données **et** une reprise simultanée de tous les lecteurs — pour un gain purement esthétique, sur un chantier qui n'en a pas besoin avant la bascule. + +**Ce qu'il faut faire à la place** : ne jamais recopier un helper de coordonnées d'un contexte à l'autre. `getStepGeometryCenter` (visitapp-web) et `GeoPointPosition` (tablet-app) se ressemblent et sont **incompatibles**. Chaque helper porte sa convention en commentaire — les lire avant de s'en servir. + +⚠️ **À rouvrir si SectionParcours arrive un jour dans une app qui lit aussi des GeoPoint** : elle aurait alors les deux conventions dans le même écran. C'est aujourd'hui le cas de `visitapp-web` et de `mymuseum-visitapp` — vérifié : chacun utilise le bon helper pour la bonne donnée, mais c'est l'endroit exact où une future erreur se logera. + **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**. @@ -361,6 +385,25 @@ Remplacé par `GetReferencedResourceIds()`, déjà implémentée sur les 13 sous 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 C2 et C3 — livrés le 2026-08-12. Le lot médias est terminé côté serveur.** `dotnet build` vert, `dotnet test` **163/163** (148 au départ, +7 pour C2, +8 pour C3). + +**C2 — backfill.** `POST /api/Resource/backfill-storage?dryRun=&instanceId=`, SuperAdmin, **`dryRun` à true par défaut** : la migration se joue sur une base vide, ce backfill sur des lignes de production. Il rend son propre inventaire plutôt que de reprendre le « 37 lignes sur 45 » du plan, invérifiable d'ici — `Orphans` (aucune URL, blob peut-être jamais téléversé) et `Unsized` (URL présente, bucket muet) sont **deux causes distinctes**, jamais additionnées. + +- ⚠️ **La méthode annoncée était inapplicable.** Le plan disait « `SizeBytes` par listing du bucket Firebase » : le serveur n'avait aucun client de stockage. Le sondage passe par HEAD sur l'URL publique, comme la migration. +- **Le sondeur est extrait, pas recopié** — `Helpers/ResourceSizeProbe.cs`, consommé par `MigrationController` *et* le backfill. Même raisonnement que L5 pour `ResourceStorage` : deux copies auraient divergé sur ce qui compte, le sort réservé aux échecs. +- ⚠️ **L'extraction a bouché un trou que personne ne cherchait.** L'original ne notait l'échec que dans son `catch`, or un HEAD sur un blob absent **ne lève pas** : il répond 404, sans `Content-Length`. Ces ressources arrivaient à 0 octet **sans figurer dans le rapport** — invisibles au quota et invisibles au diagnostic, exactement ce que le commentaire d'origine voulait empêcher. Corrigé des deux côtés ; `MigrationController` peut donc remonter quelques lignes de rapport de plus qu'avant, sur des cas réellement muets. + +**C3 — quota autoritaire.** Pré-vol sur les **deux** chemins de création, suppression du blob à `Delete`, angle mort d'`Update` tranché. + +- ⚠️ **Le pré-vol existait déjà à moitié, et ce n'était écrit nulle part.** `Upload` (multipart) contrôlait et renvoyait 413 ; `Create` (JSON) ne contrôlait rien — or **c'est le chemin qu'emprunte manager-app**, qui crée la ligne puis téléverse. Tout dépassement passait par la porte de service. +- ⚠️ **Les deux lectures du quota divergeaient.** `Upload` lisait le quota du **plan**, `GetQuota` celui de l'**instance** avec le plan en repli. Une instance à quota surchargé — le mécanisme même de l'add-on — affichait un chiffre à l'écran et se faisait bloquer sur un autre. Fermé par `Helpers/StorageQuota`, désormais seule source de vérité pour les deux. +- **`Delete` supprime le blob AVANT la ligne**, et un échec renvoie **502** en conservant la ligne. Le choix se résume ainsi : une ressource encore listée se rattrape, un blob dont plus aucune ligne ne porte le chemin est facturé sans que rien ne le désigne. Non configuré, le service renvoie `NotConfigured` — il ne fait jamais semblant d'avoir nettoyé. +- ✅ **L'angle mort laissé ouvert par C1 était une fausse crainte.** On redoutait qu'un recalcul de chemin « pointe vers un objet qui n'a pas bougé ». Or `PathFor` ne construit qu'un `pictures/{instanceId}/{resourceId}` : **le type n'entre pas dans le chemin**, il décide seulement s'il y en a un. Recalculer ne peut pas pointer ailleurs — `Update` rejoue donc `Apply`. + +**Aucun secret nouveau, contrairement à ce qu'on croyait en tranchant.** `FirebaseAdmin` était déjà référencé pour les notifications push et `Startup` charge déjà un service account via `Firebase:CredentialsPath` : `Google.Cloud.Storage.V1` réutilise le même `GoogleCredential`. Seule s'ajoute la clé `Firebase:StorageBucket`, **vide par défaut** — donc à renseigner en prod (I9), sans quoi `Delete` ne supprime rien. + +⚠️ **Deux suites à ne pas perdre** : `manager-app` supprime toujours le blob de son côté (`show_resource_popup.dart:130-137`), après coup et en avalant l'échec — c'est devenu **du code mort** depuis C3, à retirer (C6), mais **pas avant** que `Firebase:StorageBucket` soit posé. Et `dotnet restore` échoue en 401 si la source NuGet `git.dev-espaces-naturels.lu` est déclarée dans l'image Docker : elle n'a rien à voir avec ce projet, et `Google.Cloud.Storage.V1` est le premier paquet ajouté depuis longtemps — le prochain build d'image la rencontrera. + **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à. diff --git a/kanban.html b/kanban.html index 3dbbdb6..3e15446 100644 --- a/kanban.html +++ b/kanban.html @@ -455,12 +455,12 @@
4Urgent
-
3Migration v3
+
2Migration v3
5Bugs ouverts
6À tester
20Planifié
-
7Bascule prod
-
36Fait récemment
+
8Bascule prod
+
38Fait récemment
@@ -507,10 +507,11 @@
tablet-appLot K — bloque la bascule
-

tablet-app est resté sur l'ancien contrat d'API

-

Le premier vrai build, le 12/08, a fait sauter le diagnostic de cette carte. « Un seul défaut, purement Gradle, peut-être réglé par 5a6701d » était doublement faux — il tenait parce que personne n'avait lancé la commande. L'outillage Android demandait six crans (Gradle 7.5 → 8.11.1, AGP 7.2.0 → 8.9.1, Kotlin 2.0.10, jcenter mort) : fait le 12/08. Derrière, le Gradle masquait 132 erreurs Dart.

-

3 causes seulement : ConfigurationDTO.roundedValue & co. ont migré vers AppConfigurationLinkDTO (73 err.), GeoPointDTO.latitude/longitude sont devenus GeometryDTO geometry avec PostGIS (40 err.), le reste en découle. Le modèle du portage est déjà écrit dans mymuseum-visitapp (currentAppConfigurationLink, Models/visitContext.dart) — copier, pas concevoir.

-

⚠️ Pourquoi ça bloque la bascule (L19) : 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.

+

tablet-app — reste le renommage Puzzle → Game (K4)

+

Le premier vrai build, le 12/08, a fait sauter le diagnostic de cette carte. « Un seul défaut, purement Gradle, peut-être réglé par 5a6701d » était doublement faux — il tenait parce que personne n'avait lancé la commande. L'outillage Android demandait six crans (Gradle 7.5 → 8.11.1, AGP 7.2.0 → 8.9.1, Kotlin 2.0.10, jcenter mort) : fait le 12/08. Derrière, le Gradle masquait 132 erreurs Dartportage K2 livré le 12/08, elles sont toutes tombées.

+

Il ne reste que 6 erreurs, toutes PuzzleDTO / SectionType.Puzzle : c'est K4, à traiter en reprenant mymuseum-visitapp/lib/Screens/Sections/Game/. Voir la carte « Types de section manquants sur le kiosk ».

+

⚠️ Pourquoi ça bloque la bascule (L19) : les tablettes en service chez MDLF et au Fort parlent l'ancien contrat. Tant qu'un APK à jour n'est pas publié, le jour J elles ne s'arrêteraient pas proprement — cartes sans points, écrans dégradés, aucune erreur visible.

+

⚠️ Piège vérifié le 12/08 : deux conventions de coordonnées coexistent. Les GeoPointDTO sont en [lng, lat] (GeoJSON, écrit par GeometryMapper), les géométries de GuidedStep en [lat, lng] (écrit par manager-app). Les deux sont en prod, et une confusion ne lève aucune erreur — elle déplace les points sur une carte qui reste plausible. Ne jamais recopier un helper de coordonnées d'un contexte à l'autre. Détail : STATUS.md §1sexies.

v1-plan.md lot K · STATUS.md §1bis
@@ -528,18 +529,9 @@
-

Migration v3

3
+

Migration v3

2
-
-
manager-servicereprise
-

Backfill StoragePath/SizeBytes des 45 lignes

-

✅ Prérequis levé le 10/08 : Create écrit désormais StoragePath (pictures/{instanceId}/{resourceId}) et SizeBytes, types URL exclus, et manager-app envoie la taille avant l'upload. Le backfill ne sera donc pas à refaire au prochain upload. Reste le backfill lui-même, en deux moitiés : StoragePath est un simple UPDATE SQL (chemin déterministe), seul SizeBytes exige de lister le bucket. ⚠️ Mesuré le 09/08 sur 45 lignes : 37 ont un blob, 7 sont des types URL (Wikipedia, YouTube, agenda.php) sans aucun fichier — les inclure serait faux — et 1 est de type fichier sans URL. SizeBytes vaut 0 partout : le quota de stockage ne veut rien dire pour personne.

-

⚠️ Le « prérequis levé le 10/08 » n'était vrai qu'à moitié — relevé le 11/08 en extrayant le calculateur (C1). ResourceController a deux chemins de création, et le chemin multipart (le téléversement) écrivait SizeBytes mais laissait StoragePath nul ; seul le chemin JSON écrivait les deux. Les deux passent maintenant par ResourceStorage.Apply, donc le prérequis est réellement levé — mais des lignes créées entre-temps sont à rattraper par ce backfill.

-

Appeler ResourceStorage.PathFor plutôt que réécrire le chemin : c'est tout l'objet de C1, et l'écart (e) de la migration l'appellera aussi.

- v2/media-storage-plan.md §5 — État mesuré · STATUS.md §1quater — Lot médias -
-
manager-appGros gain

Compression images — 2560 px / JPEG q82

@@ -547,11 +539,11 @@ v2/media-storage-plan.md
-
-
manager-service
-

Quota de stockage réellement appliqué

-

Aujourd'hui contrôlé uniquement dans l'endpoint legacy. Pré-vol + contrôle autoritaire à Create avec la taille lue depuis Firebase + suppression du blob à Delete.

- v2/media-storage-plan.md +
+
manager-appcode mort depuis C3
+

Retirer la suppression de blob de manager-app

+

show_resource_popup.dart:130-137 supprime l'objet du bucket après avoir supprimé la ligne, et avale l'échec dans un print — un échec laissait un orphelin définitif que le quota ne comptait plus. Depuis C3 (12/08) le serveur supprime le blob avant la ligne : le client tombe désormais sur un objet déjà absent. Ce n'est plus dangereux, c'est devenu inutile. ⚠️ Ne pas le retirer avant d'avoir renseigné Firebase:StorageBucket en prod (carte « Bascule prod ») : la clé est vide par défaut, et sans elle le serveur ne supprime rien — retirer le code client ferait perdre la fonction au lieu de la déplacer.

+ v1-plan.md lot C — C6 · STATUS.md §1sexies
@@ -807,7 +799,7 @@
-

Bascule prod

7
+

Bascule prod

8
@@ -850,6 +842,14 @@ STATUS.md §1quinquies — étape 19
+
+
étape I9nouveau 12/08
+

Poser Firebase:StorageBucket, puis lancer le backfill

+

Deux gestes que C2 et C3 laissent à la mise en prod. 1) La clé Firebase:StorageBucket (<project-id>.appspot.com) est vide par défaut : sans elle, Delete ne supprime aucun blob — il ne casse rien, il ne nettoie pas. C'est aussi le verrou de la carte « retirer la suppression de blob de manager-app ». 2) Lancer le backfill : POST /api/Resource/backfill-storage?dryRun=true, relire Orphans et Unsized, puis dryRun=false. Sans lui le quota de C3 porte sur des SizeBytes à 0 pour tout l'existant, donc il laisse tout passer (L10).

+

⚠️ Piège de build repéré le 12/08 : dotnet restore échoue en 401 si la source NuGet git.dev-espaces-naturels.lu est déclarée dans l'image — elle n'a rien à voir avec ce projet. Google.Cloud.Storage.V1 étant le premier paquet ajouté depuis longtemps, le prochain build d'image la rencontrera.

+ v1-plan.md lot I — I9 · STATUS.md §1sexies +
+
étape 20Le vrai test

Vérifications post-bascule

@@ -875,9 +875,20 @@

Fait récemment

-

Trente-six chantiers clos entre le 5 et le 12 août 2026.

+

Trente-huit chantiers clos entre le 5 et le 12 août 2026.

+
+ Lot médias terminé côté serveur — backfill (C2) et quota autoritaire (C3) + C2 : POST /api/Resource/backfill-storage, SuperAdmin, dryRun à true par défaut — la migration se joue sur une base vide, ce backfill sur des lignes de production. La méthode annoncée au plan (« SizeBytes par listing du bucket Firebase ») était inapplicable : le serveur n'avait aucun client de stockage. Le sondage passe par HEAD, et le sondeur est extrait plutôt que recopié (ResourceSizeProbe, partagé avec la migration — même raisonnement que L5). ⚠️ L'extraction a bouché un trou que personne ne cherchait : l'original ne notait l'échec que dans son catch, or un HEAD sur un blob absent ne lève pas — il répond 404 sans Content-Length. Ces ressources arrivaient à 0 octet sans figurer dans le rapport. Le « 37 sur 45 » du plan n'étant pas vérifiable d'ici, le backfill rend son propre inventaire, Orphans et Unsized séparés — deux causes distinctes, jamais additionnées. + C3 : ⚠️ le pré-vol existait déjà à moitié et ce n'était écrit nulle partUpload (multipart) contrôlait, Create (JSON) ne contrôlait rien, or c'est le chemin qu'emprunte manager-app. ⚠️ Et les deux lectures du quota divergeaient : Upload lisait le plan, GetQuota l'instance avec le plan en repli — une instance à quota surchargé (le mécanisme de l'add-on) affichait un chiffre et se faisait bloquer sur un autre. Fermé par StorageQuota. Delete supprime le blob avant la ligne et renvoie 502 en la conservant si le bucket échoue : une ressource encore listée se rattrape, un blob que plus aucune ligne ne désigne est facturé à l'aveugle. ✅ L'angle mort laissé par C1 était une fausse crainte : PathFor ne construit qu'un pictures/{instanceId}/{resourceId}, le type n'entre pas dans le chemin. Aucun secret nouveau : FirebaseAdmin était déjà là pour le push, Google.Cloud.Storage.V1 réutilise le même credential. dotnet test 163/163. +
+ +
+ tablet-app rebranché sur le contrat d'API de Postgres v3 (K2) + Les 132 erreurs Dart que le Gradle masquait sont tombées : flutter analyze lib ne renvoie plus que les 6 PuzzleDTO de K4. Livré : TabletAppContext.currentAppConfigurationLink (copie du getter de mymuseum), lib/Helpers/geo.dart avec l'extension lat/lng, 67 accès roundedValue rebranchés, main_view et les 4 vues carte portées. La formule de clé par coordonnées de geo_point_filter, dupliquée à quatre endroits alors que les deux côtés de l'appariement doivent produire la même valeur, ne vit plus qu'à un seul.

⚠️ Deux bugs latents trouvés — la raison pour laquelle un chercher-remplacer aurait été dangereux. (1) applicationInstanceDTO n'était assigné que si l'instance avait l'IA : il servait de drapeau d'assistant. Ce DTO portant désormais les AppConfigurationLink, le nouveau getter aurait renvoyé null sur toute instance sans IA — donc MDLF et le Fort — et les cinq réglages d'affichage seraient retombés sur leurs défauts, sans erreur ni log. Drapeau rendu explicite (isAssistantEnabled), en préservant le ET instance × canal. (2) ConfigurationDTO.isTablet ayant disparu, le filtre « configurations pour tablette » n'avait plus de source : il porte maintenant sur les liens du canal AppType.Tablet. À valider sur device — une configuration non rattachée au canal tablette rendra l'écran de sélection vide.
+
+
tablet-app : six crans d'outillage Android rattrapés Le premier vrai build depuis des mois. Chaque erreur dictait le correctif suivant : Gradle 7.5 → 8.11.1 (le plugin Kotlin exigeait ≥ 7.6.3), AGP 7.2.0 → 8.5.0 et Kotlin 1.9.0 → 2.0.10 (onVariants$default), suppression de android.bundle.enableUncompressedNativeLibs (retirée en AGP 8.1), AGP → 8.7.3 (minimum Flutter), AGP → 8.9.1 (réclamé par androidx.browser et core-ktx), et jcenter()mavenCentral() (jcenter est arrêté depuis 2021). ⚠️ Un bloc buildscript entièrement commenté déclarait AGP 8.5.0/Kotlin 2.0.10 pendant que la vraie config vivait dans settings.gradle 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 troisième piège « bloc commenté » du projet. Ce que ça débloque : compileSdkVersion 36, sans lequel plus aucune mise à jour n'est publiable sur le store. Reste le portage Dart — voir la carte du lot K. diff --git a/v1-plan.md b/v1-plan.md index a6b7831..49ff139 100644 --- a/v1-plan.md +++ b/v1-plan.md @@ -117,9 +117,10 @@ L'étape A du §1quater est faite (`StoragePath`/`SizeBytes` écrits à `Create` | # | Quoi | Note | |---|---|---| | ~~C1~~ ✅ **2026-08-11** | **Calculateur extrait** dans `Helpers/ResourceStorage.cs` (statique, comme `ImageHelper`/`SlugHelper` — logique pure, pas d'I/O, donc appelable sans DI depuis `MigrationController` et le futur backfill). Trois membres : `HasBlob(type)`, `PathFor(type, instanceId, resourceId)`, `Apply(resource, sizeBytes)`. **13 tests** fixent l'invariant des types URL. `dotnet test` **143/143** | **L5** — consommé par C2 *et* par l'écart (e) du lot G.
⚠️ **L'extraction a révélé la divergence qu'elle devait empêcher** : il y a **deux** chemins de création dans `ResourceController`, et le chemin **multipart** (téléversement) écrivait `SizeBytes` mais laissait **`StoragePath` nul** — seul le chemin JSON écrivait les deux. Les deux passent désormais par `Apply`. C'est une partie des lignes que C2 doit rattraper.
⚠️ **Angle mort laissé volontairement** : `Update` peut faire passer `Type` d'un type URL à un type fichier, laissant `StoragePath` nul sur une ligne qui a maintenant un blob. Non corrigé ici — recalculer le chemin sur une ressource existante pointerait vers un objet qui n'a pas bougé dans le bucket. À trancher avec C3 (quota), pas avant | -| 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 | +| ~~C2~~ ✅ **2026-08-12** | **Backfill livré** : `POST /api/Resource/backfill-storage?dryRun=&instanceId=`, SuperAdmin, **`dryRun` à true par défaut** (la migration se joue sur une base vide, celui-ci sur des lignes de production). `StoragePath` par `ResourceStorage.PathFor`, `SizeBytes` par HEAD. **7 tests** | ⚠️ **La méthode annoncée ici était inapplicable** : « listing du bucket Firebase » supposait un client de stockage que le serveur n'avait pas. Le sondage passe par HEAD sur l'URL publique, comme la migration.
**Le sondeur est extrait, pas recopié** — `Helpers/ResourceSizeProbe.cs`, consommé par `MigrationController` *et* le backfill (même raisonnement que L5).
⚠️ **L'extraction a bouché un trou** : l'original ne notait l'échec que dans son `catch`, or un HEAD sur un blob absent **ne lève pas** (404 sans `Content-Length`). Ces ressources arrivaient à 0 octet **sans figurer dans le rapport**. Corrigé des deux côtés.
Le « 37 sur 45 » n'était pas vérifiable : le backfill rend son propre inventaire, `Orphans` (pas d'URL) et `Unsized` (URL muette) séparés — ce sont deux causes distinctes | +| ~~C3~~ ✅ **2026-08-12** | **Quota autoritaire livré** : pré-vol sur les **deux** chemins de création, suppression du blob à `Delete`, et l'angle mort d'`Update` tranché. **8 tests**, `dotnet test` **163/163** | **L10** respecté — après C2.
⚠️ **Le pré-vol existait déjà à moitié, non documenté** : `Upload` (multipart) contrôlait et renvoyait 413, `Create` (JSON) ne contrôlait rien — or c'est le chemin qu'emprunte manager-app. Le dépassement passait par la porte de service.
⚠️ **Et les deux lectures du quota divergeaient** : `Upload` lisait le **plan**, `GetQuota` lisait l'**instance** avec le plan en repli. Une instance à quota surchargé — le mécanisme de l'add-on — affichait un chiffre et se faisait bloquer sur un autre. Fermé par `Helpers/StorageQuota`, seule source de vérité.
✅ **L'angle mort de C1 était une fausse crainte** : `PathFor` ne produit qu'un `pictures/{instanceId}/{resourceId}` — **le type n'entre pas dans le chemin**, il décide seulement s'il y en a un. Recalculer ne peut pas pointer ailleurs. `Update` rejoue donc `Apply` | | C4 | Compression images côté client, 2560 px / JPEG q82 (~12×) | `manager-app`. Indépendant de C2 : ne touche que les nouveaux uploads | +| C6 | **Retirer la suppression de blob de `manager-app`** — `show_resource_popup.dart:130-137` | `manager-app`. Depuis C3 le serveur supprime le blob **avant** la ligne : le client tombe désormais sur un objet déjà absent. Ce n'est plus dangereux, c'est devenu **du code mort**. ⚠️ Ne pas le retirer *avant* d'avoir renseigné `Firebase:StorageBucket` en prod (I9), sinon plus personne ne supprime | | ~~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) @@ -269,7 +270,8 @@ Ordre du §2 de STATUS.md, corrigé par **L12**. | # | 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 | +| ~~**K2**~~ ✅ **2026-08-12** | **Portage livré. Les 132 erreurs de contrat sont tombées** ; `flutter analyze lib` ne renvoie plus que les 6 erreurs `PuzzleDTO`, qui sont K4. Fait : `TabletAppContext.currentAppConfigurationLink` (copie du getter mymuseum), `lib/Helpers/geo.dart` avec l'extension `lat`/`lng`, 67 accès `roundedValue` rebranchés, `main_view` et les 4 vues carte portées.
⚠️ **Deux bugs latents trouvés — c'est ce qui justifiait de le faire à la main.**
**(1)** `applicationInstanceDTO` n'était assigné **que si `instanceDTO.isAssistant == true`** (`main_view:339`) : il servait de drapeau d'assistant. Ce DTO portant désormais les `AppConfigurationLink`, le getter aurait renvoyé `null` sur **toute instance sans IA — donc MDLF et le Fort** — et les cinq réglages seraient retombés sur leurs défauts, sans erreur ni log. Corrigé : assignation inconditionnelle + drapeau explicite `isAssistantEnabled`, qui **préserve le ET** instance × canal (les deux gardes de `AiController:315/360`).
**(2)** `ConfigurationDTO.isTablet` a disparu : le filtre « configurations pour tablette » de `getConfigurations` n'avait plus de source. Il porte désormais sur les `AppConfigurationLink` du canal `AppType.Tablet`, même appel que `main_view`. ⚠️ **À valider sur device (§0/§18)** : si un client a des configurations non rattachées à son canal tablette, l'écran de sélection sera **vide**. L'ancien code avait le même angle mort, mais la donnée qui pilote le filtre a changé.
⚠️ **Piège de vérification** : `flutter analyze` écrit `error - `, pas `error • ` — un grep sur la mauvaise forme a fait conclure « 0 erreur » alors qu'il en restait 10 | +| ~~K2 — notes de conception~~ | ⚠️ **Deux conventions de coordonnées coexistent** dans le projet — `GeoPointDTO` en `[lng, lat]`, géométries de `GuidedStep` en `[lat, lng]`. Une confusion **ne lève aucune erreur**, elle déplace les points. Détail et tableau dans **STATUS.md §1sexies**. Fondations déjà posées le 2026-08-12 : `TabletAppContext.currentAppConfigurationLink` et `lib/Helpers/geo.dart`.
**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 | @@ -302,6 +304,7 @@ Les CGU §8 ont été réécrites le 2026-08-11 (`cgu-myinfomate.md`) et le text | 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 | | I4 | `dotnet ef database update` sur la base vide, puis vérifier `postgis` **et** `vector` présents | Une base en retard d'une migration fait planter l'API dès le login — c'est arrivé en local le 09/08 | +| **I9** | **Renseigner `Firebase:StorageBucket`** (`.appspot.com`) dans la config de prod, puis **lancer le backfill C2** : `POST /api/Resource/backfill-storage?dryRun=true` d'abord, relire `Orphans`/`Unsized`, puis `dryRun=false` | ⚠️ **Deux conséquences si on l'oublie.** Le clé est vide par défaut : sans elle, `Delete` **ne supprime aucun blob** — il ne casse rien, il ne nettoie pas, et C6 (retrait du code client) deviendrait une perte de fonction. Et sans le backfill, le quota de C3 porte sur des `SizeBytes` à 0 pour tout l'existant : il laisse tout passer (**L10**).
⚠️ **Piège de build repéré le 2026-08-12** : `dotnet restore` échoue (401) si la source NuGet `git.dev-espaces-naturels.lu` est déclarée dans l'image — elle est sans rapport avec ce projet. À vérifier dans le Dockerfile avant de rebuilder, le nouveau `Google.Cloud.Storage.V1` étant le premier paquet ajouté depuis longtemps | | I5 | **Plan de retour arrière écrit**, avant le jour J | « Un rollback qu'on improvise à 23 h n'est pas un rollback » | | I6 | `pg_dump` avant, puis **rejouer instance par instance** (`instanceId=…`), en commençant par la plus petite | |