Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011VxSQeGQYUvPmSEoGdnidA
8.4 KiB
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 kanbancards/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 :
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.SectionQuiz+QuizQuestion(Data/SubSection/) — structurellement un formulaire : liste ordonnée de questions traduites enjsonb, options enjsonb, unQuestionType.SectionFormc'est le même squelette moins la bonne réponse, plus le stockage.VisitorQuestionPurgeService(Services/) — le job de purge par rétention existe et tourne :FormSubmissions'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 dansServices/SectionFactory.cs(leswitchde désérialisation et leswitchde construction).Controllers/SectionFormController.cs, sur le modèle deSectionScene3DController:- CRUD admin, authentifié ;
POST /api/SectionForm/submiten[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 dansSectionQuiz(« 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 politiqueUseRateLimiter()existe déjà depuis le chantier API Keys), respect deClosingDate, 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 :
RetentionDaysparamé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.