diff --git a/kanban/cards/1-urgent/020-regles-firebase-storage-ouvertes-a-tous.md b/kanban/cards/1-urgent/020-regles-firebase-storage-ouvertes-a-tous.md new file mode 100644 index 0000000..12e7b5b --- /dev/null +++ b/kanban/cards/1-urgent/020-regles-firebase-storage-ouvertes-a-tous.md @@ -0,0 +1,15 @@ +--- +title: Le bucket Firebase est ouvert en écriture et en suppression à tous +area: backend infra +tags: manager-service, sécurité, Studio +flag: critical | À régler impérativement avant/avec la mise en production de la version actuelle +src: conversation 14/09 — relevé des règles via l'API firebaserules +--- +

Relevé le 14/09 sur le projet mymuseum-3b97f (release firebase.storage/mymuseum-3b97f.appspot.com, ruleset 025f98db-…, inchangé depuis le 08/01/2024) :

+
match /{allPaths=**} {
+  allow read, write; //: if request.time < timestamp.date(2024, 1, 12)
+}
+

La garde temporelle du template Firebase a été commentée pour qu'elle n'expire pas. N'importe qui connaissant le nom du bucket — il est dans l'URL de chaque image, donc public — peut lire, lister, écrire et supprimer le contenu de tous les clients. Vérifié sans aucune authentification : GET firebasestorage.googleapis.com/v0/b/mymuseum-3b97f.appspot.com/o répond 200 avec la liste des fichiers.

+

Pourquoi ce n'est pas déjà fermé : le manager-app déployé écrit dans le bucket sans Firebase Auth. Fermer avant de déployer casse ses uploads. La fermeture est donc séquencée après la mise en production du lot 0 du Studio (upload par URL signée + ingestion serveur), comme le prévoit v2/studio-plan.md §8. ⚠️ La date de ce déploiement est donc une date de sécurité, pas seulement une date de feature — ne pas mettre la version actuelle en production sans traiter ce point dans la foulée.

+

Piège à traiter dans le même geste : le CORS du bucket de prod n'a aucun responseHeader. Le jour du déploiement, le PUT signé portant Content-Type et x-goog-content-length-range sera refusé au préflight et tous les uploads casseront. À corriger sur le bucket avant la bascule. Le bucket de dev mymuseum-3b97f-dev, lui, est déjà configuré correctement (CORS, cycle de vie incoming/, règles fermées) et sert de modèle.

+

Une piste applicable avant le déploiement, à vérifier : restreindre read/list seul. Les apps visiteur lisent par URL à jeton, qui ne passe pas par les règles ; reste à confirmer qu'aucun code client ne lit par le SDK Firebase.

diff --git a/kanban/cards/5-planifie/285-loader-anime-presets-paremetriques.md b/kanban/cards/5-planifie/285-loader-anime-presets-paremetriques.md new file mode 100644 index 0000000..8d16f03 --- /dev/null +++ b/kanban/cards/5-planifie/285-loader-anime-presets-paremetriques.md @@ -0,0 +1,23 @@ +--- +title: Loader animé — presets paramétriques, pas de format de fichier +area: manager +horizon: v2 +tags: manager-app, visitapp, tablet, vr +flag: good | Décision arrêtée, à exécuter après la bascule +src: décision 2026-09-15 — analyse des formats de loader animé +--- +

Aujourd'hui le loader est une image fixe : Configuration.LoaderImageUrl / LoaderImageId, rendus par loading_common.dart dans les deux apps Flutter et par SplashScreen côté web. Le besoin : que le client compose un loader animé depuis le manager, sans fournir de fichier.

+ +

Décision : pas de format de fichier animé. On transporte des paramètres. Un nouveau champ de configuration porte {preset, couleurs[], logoResourceId, vitesse} — quelques centaines d'octets — et chaque front implémente nativement les 5-6 mêmes presets : AnimationController en Flutter, CSS/Canvas en Next.js, animation Unity côté casque. Le preset n°1 existe déjà et tourne : manager-app/lib/Components/loader_animated_pieces.dart (6 pièces vectorielles, onde d'opacité, rotation, flottement) — il suffit d'en sortir les couleurs et les timings, aujourd'hui en dur.

+ +

Les couleurs sont pré-remplies depuis VisualIdentityDTO.palette, qui existe déjà. Aucun crédit Studio débité : le rendu est local au navigateur, rien ne passe par un modèle.

+ +

Le SVG animé est écarté : flutter_svg ignore <animate>, SMIL et les keyframes CSS — il rendrait une image figée dans les trois apps Flutter — et Unity n'a aucun rendu SVG. Un seul des quatre fronts l'afficherait.

+ +

Lottie est écarté à ce stade, malgré son écosystème. Format de fait et non norme, gouverné par une société unique (LottieFiles) sur une spec récente ; surtout, aucun runtime moteur de jeu n'est listé sur le site officiel. Les deux options Unity sont fragiles : thorvg.unity (Android arm64 annoncé, mais 26 commits et 14 étoiles, successeur de Lottity archivé en 11/2025) et unity-rlottie (plus fourni mais en experimental, avec un ticket ouvert sur une texture NULL en build Android). Les deux rastérisent en Texture2D à chaque frame côté CPU — inacceptable sur Quest à 72-90 Hz pour un écran de chargement.

+ +

Porte de sortie assumée : le jour où un client veut apporter sa propre animation faite par une agence, on ajoute Lottie sur les 3 fronts 2D seulement, avec le PNG de première frame en repli côté VR. Le champ loader accepte alors soit des paramètres, soit une URL — rien de ce qui est fait ici n'est à refaire.

+ +

Coût réel : les presets. Chacun est du code dans 4 dépôts, à tester sur les 4 fronts. En sortir 5-6, pas 20 : au-delà le client ne choisit plus, il se perd. Stocker les paramètres permet la réédition six mois plus tard.

+ +

⚠️ Pas un bloquant V1 — le loaderImageUrl actuel fait le travail avec un PNG. À ouvrir après la bascule prod.

diff --git a/v2/section-form-plan.md b/v2/section-form-plan.md new file mode 100644 index 0000000..3dd4b63 --- /dev/null +++ b/v2/section-form-plan.md @@ -0,0 +1,177 @@ +# SectionForm — formulaires visiteurs + +> Chantier **V2**, reporté le 2026-08-07, **analysé dans le code le 2026-09-15**. +> Spec d'origine : `todo-features.md` § SectionForm · carte kanban `cards/5-planifie/240-…` +> Ce document ne rouvre pas la décision « V2 » : il dit **comment** on le fait quand on le fait. + +--- + +## Pourquoi ce type de section + +Tout le flux produit va aujourd'hui du lieu vers le visiteur. Ce qui remonte est **dérivé** : +`VisitEvent` (télémétrie anonyme) et `VisitorQuestion` (ce qui a été demandé au guide IA). +Aucun mécanisme ne permet à un lieu de **poser une question**. + +Use cases visés, dans l'ordre de valeur commerciale : enquête de satisfaction (les musées +publics en produisent pour leurs rapports de subsides), livre d'or, sondage d'exposition, +inscription à un atelier. + +--- + +## Décisions actées + +| Décision | Conséquence | +|---|---| +| **Réponses anonymes**, sans compte ni identification | Pas de champ « nom », pas de liaison à une personne. Ça allège massivement le volet RGPD — on reste sur le régime déjà décrit au §8 des CGU | +| **Les trois fronts visiteur**, kiosk compris | `mymuseum-visitapp`, `visitapp-web`, **`tablet-app`** — une borne en fin de parcours est le meilleur emplacement pour une enquête de satisfaction | +| **Table de réponses dédiée**, pas `VisitEvent.Metadata` | Rétention propre, export, suppression ciblée, requêtes par champ | +| **Les réponses ne sont jamais indexées** pour le RAG | Seuls les libellés des questions le sont | +| Pas d'upload de fichier en V1 | Les règles Firebase Storage de prod sont encore ouvertes en écriture — mauvais moment pour ajouter une porte d'upload public | + +--- + +## Ce qui sert de socle (déjà écrit) + +La moitié de la plomberie existe. Trois précédents à reprendre plutôt qu'à réinventer : + +1. **`StatsController.TrackEvent`** (`Controllers/StatsController.cs`) — le **seul** endpoint + `[AllowAnonymous]` où un visiteur écrit en base. C'est exactement le contrat de soumission + d'un formulaire : modèle à copier, y compris la validation défensive. +2. **`SectionQuiz` + `QuizQuestion`** (`Data/SubSection/`) — structurellement un formulaire : + liste ordonnée de questions traduites en `jsonb`, options en `jsonb`, un `QuestionType`. + `SectionForm` c'est le même squelette **moins** la bonne réponse, **plus** le stockage. +3. **`VisitorQuestionPurgeService`** (`Services/`) — le job de purge par rétention existe et + tourne : `FormSubmission` s'y branche sans rien inventer. + +Et le coût d'ajout d'un type de section est balisé : `Scene3D`, le dernier ajouté, a touché +11 fichiers backend et 8 dans `manager-app`. La partie réellement neuve ici, c'est **l'écran +de réponses**, qui n'a aucun équivalent. + +--- + +## Modèle de données + +``` +DTOs/SectionType.cs + Form ← ajouté EN FIN d'enum + (persisté en int : cf. le commentaire de Scene3D) + +Data/SubSection/SectionForm.cs : Section + List Fields + List IntroText jsonb + List ConfirmationText jsonb + DateTime? ClosingDate + int RetentionDays + +Data/SubSection/FormField.cs + Label jsonb traduit (même forme que QuizQuestion.Label) + Order, FieldType, IsRequired + Options jsonb (pour single_choice / multiple_choice) + +Data/FormSubmission.cs + Id, InstanceId, SectionId, ConfigurationId + SessionId, Language, Answers jsonb, Timestamp + [Index(InstanceId)] [Index(SectionId)] +``` + +`FormSubmission` vit à la racine de `Data/`, avec `VisitEvent` et `VisitorQuestion` — c'est de +la donnée de visite, pas du contenu éditorial. + +### Types de champs — V1 volontairement courte + +`text` (court), `long_text`, `single_choice`, `multiple_choice`, `rating` (1-5). + +Pas d'email, pas de date, pas d'upload. Les quatre premiers couvrent l'enquête de +satisfaction et le sondage, qui sont les deux vrais cas. + +--- + +## Backend + +- `DTOs/SubSection/FormDTO.cs` + branchement dans **`Services/SectionFactory.cs`** + (le `switch` de désérialisation **et** le `switch` de construction). +- `Controllers/SectionFormController.cs`, sur le modèle de `SectionScene3DController` : + - CRUD admin, authentifié ; + - `POST /api/SectionForm/submit` en **`[AllowAnonymous]`** ; + - `GET /api/SectionForm/{id}/submissions` (admin) — liste + agrégats ; + - `GET /api/SectionForm/{id}/export` — CSV ; + - `DELETE /api/SectionForm/{id}/submissions` — purge manuelle. +- `GetEmbeddableText` : **les libellés de questions oui, les réponses jamais.** Le + raisonnement est déjà écrit dans `SectionQuiz` (« le guide les réciterait au premier + visiteur qui demande ») ; ici ce serait pire — le guide recracherait les avis d'autres + visiteurs, y compris désobligeants, à un visiteur suivant. +- **L'endpoint de soumission est ouvert** : rate-limit par `SessionId`/IP (la politique + `UseRateLimiter()` existe déjà depuis le chantier API Keys), respect de `ClosingDate`, + et cap de soumissions par session. + +--- + +## manager-app + +Fichiers à toucher, identifiés sur le précédent `Scene3D` : + +| Fichier | Nature | +|---|---| +| `lib/client.dart` + `manager_api_new/` | client généré, **édité à la main** | +| `lib/Screens/Configurations/new_section_popup.dart` | entrée « Formulaire » | +| `lib/Components/fetch_section_icon.dart` | icône du type | +| `lib/Screens/Configurations/Section/section_detail_screen.dart` | routage vers l'éditeur | +| `lib/Screens/Configurations/Section/SubSection/Form/form_config.dart` | **neuf** — constructeur de formulaire | +| `lib/Screens/Configurations/Section/SubSection/Form/form_submissions.dart` | **neuf** — onglet réponses | +| `lib/l10n/app_{fr,en,nl}.arb` | i18n obligatoire, aucun littéral dans un widget | + +**Où vivent les réponses** : dans un **onglet de l'écran de la section**, pas dans +`Screens/Statistics/`. L'admin qui ouvre son formulaire veut ses réponses sous les yeux ; +`Statistics` répond à une autre question (la fréquentation). + +Contenu de l'onglet : en-tête d'agrégats (nombre de réponses, moyenne des `rating`, +répartition des choix en barres horizontales — la charte de `stats-screen-plan.md` +s'applique), puis la liste des textes libres, puis l'export CSV. + +--- + +## Fronts visiteur + +| Repo | Fichier | Note | +|---|---|---| +| `mymuseum-visitapp` | `lib/Screens/Sections/Form/form_page.dart` | à côté de `Quiz/quizz_page.dart` | +| `visitapp-web` | `src/components/sections/FormSection.tsx` | **obligatoire** — le plan Essentiel est web-only. Lire le Flutter d'abord, c'est la référence de comportement | +| `tablet-app` | `lib/Screens/Form/form_view.dart` | à côté de `Screens/Quizz/quizz_view.dart` | + +### Le point d'attention propre au kiosk + +Sur une borne fixe, le visiteur suivant hérite de l'écran du précédent. Même anonyme, une +saisie libre à moitié écrite qui reste affichée est un défaut visible. Il faut donc, sur +`tablet-app` uniquement : **reset du formulaire après soumission** et **reset sur +inactivité**, sur le même timer que celui qui ramène déjà la borne à l'accueil. + +--- + +## RGPD — allégé, pas nul + +L'anonymat retire le gros du sujet : pas de personne identifiée, donc pas de droit d'accès +à exercer par visiteur. Restent deux points, tous deux déjà traités ailleurs dans le produit : + +- **La saisie libre peut contenir des données personnelles spontanées.** C'est exactement ce + que le §8 des CGU écrit déjà pour le champ de question du guide IA — la même clause couvre + ce cas, il suffit de l'étendre au formulaire lors de la relecture juridique déjà planifiée + (carte `195-faire-valider-les-cgu-par-un-juriste`). +- **Rétention** : `RetentionDays` paramétrable + purge automatique via le service existant. + +--- + +## Hors périmètre V1 du chantier + +Logique conditionnelle entre champs, pages multiples, notification email à chaque réponse, +upload de fichier, réponses liées à un visiteur identifié. Le risque de ce chantier est de +glisser vers « on refait Google Forms » — or la valeur ici n'est pas la richesse de +l'éditeur, c'est d'être **déclenchable dans le parcours de visite** (fin d'un parcours +guidé, beacon de sortie, borne de fin). + +--- + +## Positionnement commercial + +À réserver aux plans **Pro et supérieurs** : la collecte de retours a une valeur propre, et +le plan Essentiel est déjà le plan « vitrine ». À arbitrer avec la grille de +`myinfomate-landing`, qui reste la source de vérité des prix.