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:
parent
6221978526
commit
556391cf59
@ -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 < 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>
|
||||||
@ -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><animate></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
177
v2/section-form-plan.md
Normal 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.
|
||||||
Loading…
x
Reference in New Issue
Block a user