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 | |