Docs en attente : plan SectionForm, cartes 020 (règles Storage) et 285 (loader)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011VxSQeGQYUvPmSEoGdnidA
This commit is contained in:
Thomas Fransolet 2026-09-15 20:09:53 +02:00
parent 6221978526
commit 556391cf59
3 changed files with 215 additions and 0 deletions

View File

@ -0,0 +1,15 @@
---
title: Le bucket Firebase est <strong>ouvert en écriture et en suppression à tous</strong>
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
---
<p>Relevé le 14/09 sur le projet <code>mymuseum-3b97f</code> (release <code>firebase.storage/mymuseum-3b97f.appspot.com</code>, ruleset <code>025f98db-…</code>, <strong>inchangé depuis le 08/01/2024</strong>) :</p>
<pre>match /{allPaths=**} {
allow read, write; //: if request.time &lt; timestamp.date(2024, 1, 12)
}</pre>
<p>La garde temporelle du template Firebase a été <strong>commentée</strong> 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 <strong>lire, lister, écrire et supprimer</strong> le contenu de tous les clients. Vérifié sans aucune authentification : <code>GET firebasestorage.googleapis.com/v0/b/mymuseum-3b97f.appspot.com/o</code> répond <strong>200</strong> avec la liste des fichiers.</p>
<p><strong>Pourquoi ce n'est pas déjà fermé</strong> : le manager-app déployé écrit dans le bucket <strong>sans Firebase Auth</strong>. Fermer avant de déployer casse ses uploads. La fermeture est donc séquencée <strong>après</strong> la mise en production du lot 0 du Studio (upload par URL signée + ingestion serveur), comme le prévoit <code>v2/studio-plan.md</code> §8. ⚠️ <strong>La date de ce déploiement est donc une date de sécurité, pas seulement une date de feature</strong> — ne pas mettre la version actuelle en production sans traiter ce point dans la foulée.</p>
<p><strong>Piège à traiter dans le même geste</strong> : le CORS du bucket de prod n'a <strong>aucun <code>responseHeader</code></strong>. Le jour du déploiement, le <code>PUT</code> signé portant <code>Content-Type</code> et <code>x-goog-content-length-range</code> sera refusé au préflight et <strong>tous les uploads casseront</strong>. À corriger sur le bucket avant la bascule. Le bucket de dev <code>mymuseum-3b97f-dev</code>, lui, est déjà configuré correctement (CORS, cycle de vie <code>incoming/</code>, règles fermées) et sert de modèle.</p>
<p>Une piste applicable <strong>avant</strong> le déploiement, à vérifier : restreindre <code>read</code>/<code>list</code> 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.</p>

View File

@ -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é
---
<p>Aujourd'hui le loader est une image fixe : <code>Configuration.LoaderImageUrl</code> / <code>LoaderImageId</code>, rendus par <code>loading_common.dart</code> dans les deux apps Flutter et par <code>SplashScreen</code> côté web. Le besoin : que le client compose un loader <em>animé</em> depuis le manager, sans fournir de fichier.</p>
<p><strong>Décision : pas de format de fichier animé. On transporte des paramètres.</strong> Un nouveau champ de configuration porte <code>{preset, couleurs[], logoResourceId, vitesse}</code> — quelques centaines d'octets — et chaque front implémente nativement les 5-6 mêmes presets : <code>AnimationController</code> en Flutter, CSS/Canvas en Next.js, animation Unity côté casque. Le preset n°1 existe déjà et tourne : <code>manager-app/lib/Components/loader_animated_pieces.dart</code> (6 pièces vectorielles, onde d'opacité, rotation, flottement) — il suffit d'en sortir les couleurs et les timings, aujourd'hui en dur.</p>
<p>Les couleurs sont pré-remplies depuis <code>VisualIdentityDTO.palette</code>, qui existe déjà. Aucun crédit Studio débité : le rendu est local au navigateur, rien ne passe par un modèle.</p>
<p><strong>Le SVG animé est écarté</strong> : <code>flutter_svg</code> ignore <code>&lt;animate&gt;</code>, 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.</p>
<p><strong>Lottie est écarté à ce stade</strong>, malgré son écosystème. Format de fait et non norme, gouverné par une société unique (LottieFiles) sur une spec récente ; surtout, <em>aucun runtime moteur de jeu n'est listé sur le site officiel</em>. Les deux options Unity sont fragiles : <code>thorvg.unity</code> (Android arm64 annoncé, mais 26 commits et 14 étoiles, successeur de <code>Lottity</code> archivé en 11/2025) et <code>unity-rlottie</code> (plus fourni mais en <em>experimental</em>, avec un ticket ouvert sur une texture NULL en build Android). Les deux rastérisent en <code>Texture2D</code> à chaque frame côté CPU — inacceptable sur Quest à 72-90 Hz pour un écran de chargement.</p>
<p><strong>Porte de sortie assumée</strong> : le jour où un client veut apporter <em>sa</em> 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.</p>
<p><strong>Coût réel : les presets.</strong> 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.</p>
<p>⚠️ <strong>Pas un bloquant V1</strong> — le <code>loaderImageUrl</code> actuel fait le travail avec un PNG. À ouvrir après la bascule prod.</p>

177
v2/section-form-plan.md Normal file
View File

@ -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<FormField> Fields
List<TranslationAndResourceDTO> IntroText jsonb
List<TranslationAndResourceDTO> 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.