Compare commits
No commits in common. "ad73aefe380fd56157da3a60c5140253ac8a95bc" and "d879766cae3ba1e70333561cdfc7323391f3033d" have entirely different histories.
ad73aefe38
...
d879766cae
@ -178,7 +178,6 @@ Dix-neuf chantiers de canaux et d'IA, chacun avec son plan détaillé. État au
|
|||||||
- ✅ **`XR-3` est clos le 2026-09-12** : le JSON d'export est un **contrat versionné** (`exportVersion: 1`, `generatedAt`, règles de compatibilité écrites sur le DTO), et la question multilingue est tranchée par le code lui-même.
|
- ✅ **`XR-3` est clos le 2026-09-12** : le JSON d'export est un **contrat versionné** (`exportVersion: 1`, `generatedAt`, règles de compatibilité écrites sur le DTO), et la question multilingue est tranchée par le code lui-même.
|
||||||
- ✅ **`XR-5` est clos aussi** : **pousser une configuration vers le casque sans MQTT** — la réponse au battement porte la configuration assignée, le casque compare et recharge en revenant au menu, sans redémarrage. Coût : trois minutes de latence au pire. Ce que ça évite : une connexion permanente sur une borne et une bibliothèque MQTT dans le build IL2CPP. Le publish serveur existe déjà si un jour trois minutes sont trop. Plus l'**alerte « casque muet »** après une heure sans battement — elle corrige un mensonge, la pastille « connecté » venant du dernier battement reçu.
|
- ✅ **`XR-5` est clos aussi** : **pousser une configuration vers le casque sans MQTT** — la réponse au battement porte la configuration assignée, le casque compare et recharge en revenant au menu, sans redémarrage. Coût : trois minutes de latence au pire. Ce que ça évite : une connexion permanente sur une borne et une bibliothèque MQTT dans le build IL2CPP. Le publish serveur existe déjà si un jour trois minutes sont trop. Plus l'**alerte « casque muet »** après une heure sans battement — elle corrige un mensonge, la pastille « connecté » venant du dernier battement reçu.
|
||||||
- ➡️ **Prochain travail sur le canal XR** : **jouer le §25 du [plan de test](test-plan.md)** — 11 blocs, **135 cas**. Le §25.7 (déclarer une 360 dans le manager) et le §25.9.7 (appairer une nouvelle tablette) se jouent **sans casque** ; le reste demande le Quest. **Le POC est écrit en entier** : rien de E2 à E10 n'a jamais tourné, et c'est la seule chose qui manque.
|
- ➡️ **Prochain travail sur le canal XR** : **jouer le §25 du [plan de test](test-plan.md)** — 11 blocs, **135 cas**. Le §25.7 (déclarer une 360 dans le manager) et le §25.9.7 (appairer une nouvelle tablette) se jouent **sans casque** ; le reste demande le Quest. **Le POC est écrit en entier** : rien de E2 à E10 n'a jamais tourné, et c'est la seule chose qui manque.
|
||||||
- 🔴 **Quatre écarts entre l'onglet VR du manager et le casque, relevés dans le code le 2026-09-16** — [v2/vr-menu-bento-plan.md](v2/vr-menu-bento-plan.md), carte kanban 340. Ils ont tous la même cause : `XR-2` a réutilisé `AppConfigurationLinkScreen` tel quel (« zéro code neuf »), et cet écran sert quatre canaux qui n'ont pas les mêmes réglages. **Un** : le **sélecteur de forme bento ne pilote rien en VR** — les spans sont portés par le *lien de configuration* (`AppConfigurationLink.cs:35`, « Specific Mobile & Web ») et n'alimentent que l'écran « choisis ta configuration », qui **n'existe pas dans le casque** : un casque est appairé à une seule configuration, et le menu affiche les *sections*. `ConfigurationExport` n'a aucun champ span. **Deux** : la notification annonce « application mobile » — le paramètre `appUpdatedLabel` existe et n'est pas passé, ~15 min. **Trois** : la popup d'édition d'un casque est celle du kiosk, et **la moitié de ses champs est morte en VR** (couleurs, loader, arrondis, place des sections, heure, date — `grep` sur `Assets/Scripts` : zéro occurrence de chacun). **Quatre**, qui découle du premier : pour qu'un bento ait un sens en VR il faut des **spans par section** (backend + manager), puis un placement dense mappé sur l'arc côté Unity — l'arc est conservé, seul le calcul des positions change. Ordre : notification → popup → spans → Unity ; mini-prompts en fin de plan.
|
|
||||||
- **Abandonné le 2026-09-02** — talking head : son lipsync reposait sur `enable_time_pointing` de Google Cloud TTS, or le code livré tourne sur Gemini TTS. Le plan était sans fondation, pas « à faire ».
|
- **Abandonné le 2026-09-02** — talking head : son lipsync reposait sur `enable_time_pointing` de Google Cloud TTS, or le code livré tourne sur Gemini TTS. Le plan était sans fondation, pas « à faire ».
|
||||||
|
|
||||||
→ **Détail complet, la table des 19 chantiers : [status/roadmap-canaux.md](status/roadmap-canaux.md)**
|
→ **Détail complet, la table des 19 chantiers : [status/roadmap-canaux.md](status/roadmap-canaux.md)**
|
||||||
|
|||||||
45
kanban.html
45
kanban.html
@ -462,7 +462,7 @@
|
|||||||
<div class="stat"><span class="n n-info">1</span><span class="k">Migration v3</span></div>
|
<div class="stat"><span class="n n-info">1</span><span class="k">Migration v3</span></div>
|
||||||
<div class="stat"><span class="n n-warn">2</span><span class="k">Bugs ouverts</span></div>
|
<div class="stat"><span class="n n-warn">2</span><span class="k">Bugs ouverts</span></div>
|
||||||
<div class="stat"><span class="n">12</span><span class="k">À tester</span></div>
|
<div class="stat"><span class="n">12</span><span class="k">À tester</span></div>
|
||||||
<div class="stat"><span class="n">43</span><span class="k">Planifié</span></div>
|
<div class="stat"><span class="n">40</span><span class="k">Planifié</span></div>
|
||||||
<div class="stat"><span class="n n-gate">9</span><span class="k">Bascule prod</span></div>
|
<div class="stat"><span class="n n-gate">9</span><span class="k">Bascule prod</span></div>
|
||||||
<div class="stat"><span class="n n-good">66</span><span class="k">Fait récemment</span></div>
|
<div class="stat"><span class="n n-good">66</span><span class="k">Fait récemment</span></div>
|
||||||
</section>
|
</section>
|
||||||
@ -706,7 +706,7 @@
|
|||||||
|
|
||||||
<!-- PLANIFIÉ -->
|
<!-- PLANIFIÉ -->
|
||||||
<section class="col" style="--stripe: var(--ink-3)">
|
<section class="col" style="--stripe: var(--ink-3)">
|
||||||
<div class="col-head"><h2>Planifié</h2><span class="count">43</span></div>
|
<div class="col-head"><h2>Planifié</h2><span class="count">40</span></div>
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
|
|
||||||
<article class="card" data-area="visitapp" data-horizon="v1">
|
<article class="card" data-area="visitapp" data-horizon="v1">
|
||||||
@ -975,17 +975,6 @@
|
|||||||
<span class="src">todo-features.md — AR</span>
|
<span class="src">todo-features.md — AR</span>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="card" data-area="backend manager visitapp" data-horizon="v2">
|
|
||||||
<div class="card-meta"><span class="tag">visitapp</span><span class="tag">matériel</span><span class="flag f-warn">Nouveau 16/09 · le gros du coût est les Universal/App Links, partagés avec le QR</span></div>
|
|
||||||
<h3>Tags NFC comme déclencheurs de section</h3>
|
|
||||||
<p><strong>Troisième déclencheur physique, à côté du QR et du beacon.</strong> Analyse et faisabilité : <a href="../v2/nfc-triggers-plan.md">v2/nfc-triggers-plan.md</a>.</p>
|
|
||||||
<p>⚠️ <strong>Deux recadrages par rapport à la formulation initiale.</strong> (1) Le déclencheur n'est pas un « POI » : dans MyInfoMate le POI est un point posé sur un GLB (VR/3D, <code>immersif-frontiere-plan.md</code>). Le déclencheur physique est une <strong>Section</strong> — <code>Section.IsBeacon</code> / <code>BeaconId</code>, lu par <code>geo_beacon_trigger_service.dart:177</code>. Le tag NFC se branche au même endroit que le beacon. (2) L'URL <code>/t/{instanceId}/{code}</code> et sa table de mapping <strong>n'ont pas d'équivalent existant à réutiliser</strong> : les QR d'aujourd'hui portent l'id de section directement dans l'URL (trois formats coexistants, <code>ScannerDialog.dart:31-38</code>), il n'y a aucun endpoint de résolution <code>code → section</code>. <strong>v1 = le tag porte la même URL que le QR</strong>, donc zéro entité, zéro endpoint, zéro CRUD ; l'indirection ne se justifie que si un client demande un parc de tags réassignables — et elle vaut bien moins qu'en QR, puisque réécrire un NTAG prend deux secondes alors qu'un QR se réimprime.</p>
|
|
||||||
<p><strong>Le vrai coût est ailleurs : les Universal Links (iOS) et App Links (Android) n'existent pas.</strong> Aucun <code>assetlinks.json</code> ni <code>apple-app-site-association</code> sur nos domaines (le seul du workspace est dans <code>OpenGlasses</code>, qui n'est pas à nous), et <code>app_links</code> n'est branché que sur le retour de deep link Meta AI (<code>main.dart:69</code>). ✅ <strong>Ce lot bénéficie autant au QR</strong> : aujourd'hui un QR scanné par l'appareil photo natif n'ouvre pas l'app.</p>
|
|
||||||
<p><strong>Dépendance</strong> : le fallback web d'un tag scanné sans app installée, c'est <code>visitapp-web</code> — <strong>pas encore déployé</strong> (carte « Déployer le visiteur web »). Sans lui, un tag scanné par un visiteur sans l'app tombe dans le vide.</p>
|
|
||||||
<p><strong>Ce qui ne marchera pas, à dire au commercial</strong> : pas de lecture NFC depuis <code>visitapp-web</code> (WebNFC est Chrome/Android uniquement, jamais Safari), et l'écriture de tags depuis iOS est trop contrainte — <strong>l'écran admin de programmation sera Android-only</strong>. En contrepartie, iPhone XS+ lit un tag NDEF <strong>sans aucune app</strong>, ce qui est l'argument réel du canal.</p>
|
|
||||||
<span class="src">nfc-triggers-plan.md · ScannerDialog.dart:31-38 · Section.cs:57 · main.dart:69</span>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="card" data-area="manager" data-horizon="v2">
|
<article class="card" data-area="manager" data-horizon="v2">
|
||||||
<div class="card-meta"><span class="tag">produit</span><span class="flag f-warn">V2 · nouveau 12/08</span></div>
|
<div class="card-meta"><span class="tag">produit</span><span class="flag f-warn">V2 · nouveau 12/08</span></div>
|
||||||
<h3>Écran SuperAdmin — add-on IA & canaux d'une instance</h3>
|
<h3>Écran SuperAdmin — add-on IA & canaux d'une instance</h3>
|
||||||
@ -1011,17 +1000,6 @@
|
|||||||
<span class="src">v2/studio-plan.md — §8 (exécutable lots 0-4), §9 (catalogue v0), décisions 22-23</span>
|
<span class="src">v2/studio-plan.md — §8 (exécutable lots 0-4), §9 (catalogue v0), décisions 22-23</span>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="card" data-area="backend manager" data-horizon="v2">
|
|
||||||
<div class="card-meta"><span class="tag">studio</span><span class="tag">crédits</span><span class="flag f-warn">Nouveau 16/09 · le plafond dur a été retiré exprès (décision 22)</span></div>
|
|
||||||
<h3>Seuil d'alerte sur le solde de crédits Studio (pas un nouveau plafond)</h3>
|
|
||||||
<p><strong>La limite demandée existe déjà, deux fois.</strong> Avec des crédits prépayés (décision 17), <strong>le solde <em>est</em> le plafond</strong> — c'est exactement pour ça que la décision 22 a retiré le plafond dur organisation le 13/09. Et le garde-fou contre le stagiaire qui vide le budget en une après-midi, c'est <code>Instance.StudioPerUserDailyCap</code>, déjà appliqué dans <code>StudioCreditService.ReserveAsync</code> (<code>CreditError.DailyCapReached</code>). Rien à ajouter de ce côté : <strong>ne pas recréer un plafond organisation.</strong></p>
|
|
||||||
<p>⚠️ <strong>Le modèle n'est pas celui de Google AI Studio ni de fal.ai.</strong> Là-bas on est en <strong>postpayé</strong> : sans budget alert ni hard cap, on découvre la facture. Ici le client a payé avant ; il ne peut pas se faire surprendre par une dépense, seulement par une <strong>indisponibilité</strong> — et ça, ça ne se règle pas avec un plafond, mais avec un préavis.</p>
|
|
||||||
<p><strong>Ce qui manque vraiment : le préavis.</strong> Aujourd'hui aucune alerte n'existe. <code>studio_credits_section.dart</code> affiche disponible / solde / réservé / expiration, mais il faut aller ouvrir l'écran Facturation pour les voir ; une génération qui échoue en <code>InsufficientCredits</code> est le premier signal reçu par le conservateur. Et surtout, <strong>l'expiration à 12 mois (décision 17) est le vrai piège</strong> : un solde qui s'évapore sans prévenir, sur une institution publique, c'est un litige — pas une notification manquée.</p>
|
|
||||||
<p><strong>À faire</strong> : <code>Instance.StudioCreditsAlertThreshold</code> (en crédits, 0 = désactivé) + <code>StudioCreditsAlertSentAt</code> pour ne pas renvoyer le mail à chaque génération ; déclenchement dans <code>StudioCreditService.Record</code> quand le solde passe sous le seuil, et dans <code>ExpireDueAsync</code> (déjà un job quotidien) pour un rappel d'expiration <strong>à J-30</strong>. Envoi par <code>IEmailService</code> / <code>ResendEmailService</code>, déjà en place. Côté manager-app : un champ dans l'écran Facturation et un bandeau dans le popover de la jauge du pied de menu, i18n FR/EN/NL.</p>
|
|
||||||
<p><strong>Petit lot</strong> : 2 colonnes, un hook, un template de mail, un champ d'UI. À faire <em>après</em> la fixation des prix (décision 16, fin du lot 3) — un seuil en crédits n'a pas de valeur par défaut tant que l'unité de crédit n'est pas arrêtée.</p>
|
|
||||||
<span class="src">studio-plan.md décisions 17 et 22 · StudioCreditService.cs · studio_credits_section.dart</span>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="card" data-area="manager" data-horizon="v2">
|
<article class="card" data-area="manager" data-horizon="v2">
|
||||||
<div class="card-meta"><span class="tag">manager-app</span><span class="tag">visitapp</span><span class="tag">tablet</span><span class="tag">vr</span><span class="flag f-good">Décision arrêtée, à exécuter après la bascule</span></div>
|
<div class="card-meta"><span class="tag">manager-app</span><span class="tag">visitapp</span><span class="tag">tablet</span><span class="tag">vr</span><span class="flag f-good">Décision arrêtée, à exécuter après la bascule</span></div>
|
||||||
<h3>Loader animé — presets paramétriques, pas de format de fichier</h3>
|
<h3>Loader animé — presets paramétriques, pas de format de fichier</h3>
|
||||||
@ -1214,25 +1192,6 @@
|
|||||||
<span class="src">myinfomate-landing/audit-seo-geo.md action 19 · DOCS/v2/vr-quest-unity-plan.md · DOCS/rayban-meta-integration.md</span>
|
<span class="src">myinfomate-landing/audit-seo-geo.md action 19 · DOCS/v2/vr-quest-unity-plan.md · DOCS/rayban-meta-integration.md</span>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="card" data-area="doc" data-horizon="v2">
|
|
||||||
<div class="card-meta"><span class="tag">XR</span><span class="tag">manager-app</span><span class="tag">manager-service</span><span class="flag f-warn">V2 · 4 écarts entre l'onglet VR et le casque</span></div>
|
|
||||||
<h3>Le menu VR n'est pas ce que le manager laisse configurer</h3>
|
|
||||||
<p>L'onglet <strong>Main / VR</strong> réutilise <code>AppConfigurationLinkScreen</code>, l'écran de configuration partagé avec le mobile, le web et le kiosk (choix assumé de <code>XR-2</code> : « zéro code neuf »). Il en hérite <strong>quatre comportements qui ne correspondent à rien en VR</strong>, relevés dans le code le 2026-09-16.</p>
|
|
||||||
|
|
||||||
<p>🔴 <strong>Le sélecteur de forme bento ne pilote rien dans le casque.</strong> Les spans sont portés par le <strong>lien de configuration</strong> (<code>AppConfigurationLink.cs:35</code>, commentaire d'origine : « Specific Mobile & Web ») et ne servent qu'à l'écran « choisis ta configuration » du mobile et du web. Or un casque est appairé à <strong>une seule</strong> configuration : <strong>cet écran n'existe pas en VR</strong>. Le menu affiche les <em>sections</em> racines, toutes à la même taille — <code>grep -r "Span" Assets/Scripts</code> ne rend rien, <code>ConfigurationExport</code> n'a aucun champ span.</p>
|
|
||||||
|
|
||||||
<p>➡️ <strong>Décision : le bento VR doit porter sur les sections</strong>, pas sur les configurations. Sans ça les boutons resteront décoratifs quoi qu'on fasse. Ça demande <code>GridColSpan</code>/<code>GridRowSpan</code> sur <code>Section</code> (entité, DTO, migration, client API édité à la main) — l'export les transporte ensuite gratuitement — puis un placement dense par spans côté Unity. <strong>L'arc est conservé</strong> : ce n'est pas un défaut de rendu, un mur plat se regarde de biais sur ses bords. La grille est calculée en cellules, puis chaque cellule est <em>mappée</em> sur un angle et une hauteur. Cible <strong>6 colonnes × 2 rangées</strong> — le confort visuel en VR c'est ±60°, ce qui donne naturellement le format paysage proche du rendu web.</p>
|
|
||||||
|
|
||||||
<p>🟠 <strong>La notification annonce la mauvaise app.</strong> Modifier un contenu VR affiche « application mobile mise à jour ». Le paramètre existe déjà (<code>appUpdatedLabel</code>, <code>app_configuration_link_screen.dart:144</code>) et n'est simplement pas passé par <code>vr_screen.dart</code>. Une clé i18n et quatre lignes — <strong>~15 minutes</strong>.</p>
|
|
||||||
|
|
||||||
<p>🟠 <strong>La popup d'édition d'un casque est celle du kiosk.</strong> Largeur figée à 580, une colonne scrollée, et deux boutons dont les libellés « Annuler »/« Changer » sont <strong>en dur</strong>. Mais le vrai problème n'est pas la mise en page : <strong>la moitié des champs est morte en VR</strong> — couleur principale, couleur de fond, loader, pourcentage d'arrondis, place des sections, fond d'image, heure, date. <code>grep -r</code> sur <code>Assets/Scripts</code> : <strong>zéro occurrence</strong> de chacun. Un casque n'a ni écran de chargement 2D ni horloge d'accueil. À remplacer par une popup VR dédiée, paysage deux colonnes.</p>
|
|
||||||
|
|
||||||
<p>⚠️ <strong>Ce que ce chantier dit du reste</strong> : les quatre écarts ont la même cause — un écran partagé par quatre canaux qui n'ont pas les mêmes réglages. Le plan recommande d'<strong>en sortir</strong> pour la VR plutôt que d'y ajouter un quatrième cas particulier.</p>
|
|
||||||
|
|
||||||
<p><strong>Ordre</strong> : la notification (indépendante, 15 min) → la popup (indépendante) → les spans backend + manager → le rendu Unity (rien de visible avant). Quatre mini-prompts de lancement sont en fin de plan.</p>
|
|
||||||
<span class="src">v2/vr-menu-bento-plan.md · relevé dans le code le 2026-09-16</span>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
title: Tags NFC comme déclencheurs de section
|
|
||||||
area: backend manager visitapp
|
|
||||||
horizon: v2
|
|
||||||
tags: visitapp, matériel
|
|
||||||
flag: warn | Nouveau 16/09 · le gros du coût est les Universal/App Links, partagés avec le QR
|
|
||||||
src: nfc-triggers-plan.md · ScannerDialog.dart:31-38 · Section.cs:57 · main.dart:69
|
|
||||||
---
|
|
||||||
<p><strong>Troisième déclencheur physique, à côté du QR et du beacon.</strong> Analyse et faisabilité : <a href="../v2/nfc-triggers-plan.md">v2/nfc-triggers-plan.md</a>.</p>
|
|
||||||
<p>⚠️ <strong>Deux recadrages par rapport à la formulation initiale.</strong> (1) Le déclencheur n'est pas un « POI » : dans MyInfoMate le POI est un point posé sur un GLB (VR/3D, <code>immersif-frontiere-plan.md</code>). Le déclencheur physique est une <strong>Section</strong> — <code>Section.IsBeacon</code> / <code>BeaconId</code>, lu par <code>geo_beacon_trigger_service.dart:177</code>. Le tag NFC se branche au même endroit que le beacon. (2) L'URL <code>/t/{instanceId}/{code}</code> et sa table de mapping <strong>n'ont pas d'équivalent existant à réutiliser</strong> : les QR d'aujourd'hui portent l'id de section directement dans l'URL (trois formats coexistants, <code>ScannerDialog.dart:31-38</code>), il n'y a aucun endpoint de résolution <code>code → section</code>. <strong>v1 = le tag porte la même URL que le QR</strong>, donc zéro entité, zéro endpoint, zéro CRUD ; l'indirection ne se justifie que si un client demande un parc de tags réassignables — et elle vaut bien moins qu'en QR, puisque réécrire un NTAG prend deux secondes alors qu'un QR se réimprime.</p>
|
|
||||||
<p><strong>Le vrai coût est ailleurs : les Universal Links (iOS) et App Links (Android) n'existent pas.</strong> Aucun <code>assetlinks.json</code> ni <code>apple-app-site-association</code> sur nos domaines (le seul du workspace est dans <code>OpenGlasses</code>, qui n'est pas à nous), et <code>app_links</code> n'est branché que sur le retour de deep link Meta AI (<code>main.dart:69</code>). ✅ <strong>Ce lot bénéficie autant au QR</strong> : aujourd'hui un QR scanné par l'appareil photo natif n'ouvre pas l'app.</p>
|
|
||||||
<p><strong>Dépendance</strong> : le fallback web d'un tag scanné sans app installée, c'est <code>visitapp-web</code> — <strong>pas encore déployé</strong> (carte « Déployer le visiteur web »). Sans lui, un tag scanné par un visiteur sans l'app tombe dans le vide.</p>
|
|
||||||
<p><strong>Ce qui ne marchera pas, à dire au commercial</strong> : pas de lecture NFC depuis <code>visitapp-web</code> (WebNFC est Chrome/Android uniquement, jamais Safari), et l'écriture de tags depuis iOS est trop contrainte — <strong>l'écran admin de programmation sera Android-only</strong>. En contrepartie, iPhone XS+ lit un tag NDEF <strong>sans aucune app</strong>, ce qui est l'argument réel du canal.</p>
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
title: Seuil d'alerte sur le solde de crédits Studio (pas un nouveau plafond)
|
|
||||||
area: backend manager
|
|
||||||
horizon: v2
|
|
||||||
tags: studio, crédits
|
|
||||||
flag: warn | Nouveau 16/09 · le plafond dur a été retiré exprès (décision 22)
|
|
||||||
src: studio-plan.md décisions 17 et 22 · StudioCreditService.cs · studio_credits_section.dart
|
|
||||||
---
|
|
||||||
<p><strong>La limite demandée existe déjà, deux fois.</strong> Avec des crédits prépayés (décision 17), <strong>le solde <em>est</em> le plafond</strong> — c'est exactement pour ça que la décision 22 a retiré le plafond dur organisation le 13/09. Et le garde-fou contre le stagiaire qui vide le budget en une après-midi, c'est <code>Instance.StudioPerUserDailyCap</code>, déjà appliqué dans <code>StudioCreditService.ReserveAsync</code> (<code>CreditError.DailyCapReached</code>). Rien à ajouter de ce côté : <strong>ne pas recréer un plafond organisation.</strong></p>
|
|
||||||
<p>⚠️ <strong>Le modèle n'est pas celui de Google AI Studio ni de fal.ai.</strong> Là-bas on est en <strong>postpayé</strong> : sans budget alert ni hard cap, on découvre la facture. Ici le client a payé avant ; il ne peut pas se faire surprendre par une dépense, seulement par une <strong>indisponibilité</strong> — et ça, ça ne se règle pas avec un plafond, mais avec un préavis.</p>
|
|
||||||
<p><strong>Ce qui manque vraiment : le préavis.</strong> Aujourd'hui aucune alerte n'existe. <code>studio_credits_section.dart</code> affiche disponible / solde / réservé / expiration, mais il faut aller ouvrir l'écran Facturation pour les voir ; une génération qui échoue en <code>InsufficientCredits</code> est le premier signal reçu par le conservateur. Et surtout, <strong>l'expiration à 12 mois (décision 17) est le vrai piège</strong> : un solde qui s'évapore sans prévenir, sur une institution publique, c'est un litige — pas une notification manquée.</p>
|
|
||||||
<p><strong>À faire</strong> : <code>Instance.StudioCreditsAlertThreshold</code> (en crédits, 0 = désactivé) + <code>StudioCreditsAlertSentAt</code> pour ne pas renvoyer le mail à chaque génération ; déclenchement dans <code>StudioCreditService.Record</code> quand le solde passe sous le seuil, et dans <code>ExpireDueAsync</code> (déjà un job quotidien) pour un rappel d'expiration <strong>à J-30</strong>. Envoi par <code>IEmailService</code> / <code>ResendEmailService</code>, déjà en place. Côté manager-app : un champ dans l'écran Facturation et un bandeau dans le popover de la jauge du pied de menu, i18n FR/EN/NL.</p>
|
|
||||||
<p><strong>Petit lot</strong> : 2 colonnes, un hook, un template de mail, un champ d'UI. À faire <em>après</em> la fixation des prix (décision 16, fin du lot 3) — un seuil en crédits n'a pas de valeur par défaut tant que l'unité de crédit n'est pas arrêtée.</p>
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
title: Le menu VR n'est pas ce que le manager laisse configurer
|
|
||||||
area: doc
|
|
||||||
horizon: v2
|
|
||||||
tags: XR, manager-app, manager-service
|
|
||||||
flag: warn | V2 · 4 écarts entre l'onglet VR et le casque
|
|
||||||
src: v2/vr-menu-bento-plan.md · relevé dans le code le 2026-09-16
|
|
||||||
---
|
|
||||||
<p>L'onglet <strong>Main / VR</strong> réutilise <code>AppConfigurationLinkScreen</code>, l'écran de configuration partagé avec le mobile, le web et le kiosk (choix assumé de <code>XR-2</code> : « zéro code neuf »). Il en hérite <strong>quatre comportements qui ne correspondent à rien en VR</strong>, relevés dans le code le 2026-09-16.</p>
|
|
||||||
|
|
||||||
<p>🔴 <strong>Le sélecteur de forme bento ne pilote rien dans le casque.</strong> Les spans sont portés par le <strong>lien de configuration</strong> (<code>AppConfigurationLink.cs:35</code>, commentaire d'origine : « Specific Mobile & Web ») et ne servent qu'à l'écran « choisis ta configuration » du mobile et du web. Or un casque est appairé à <strong>une seule</strong> configuration : <strong>cet écran n'existe pas en VR</strong>. Le menu affiche les <em>sections</em> racines, toutes à la même taille — <code>grep -r "Span" Assets/Scripts</code> ne rend rien, <code>ConfigurationExport</code> n'a aucun champ span.</p>
|
|
||||||
|
|
||||||
<p>➡️ <strong>Décision : le bento VR doit porter sur les sections</strong>, pas sur les configurations. Sans ça les boutons resteront décoratifs quoi qu'on fasse. Ça demande <code>GridColSpan</code>/<code>GridRowSpan</code> sur <code>Section</code> (entité, DTO, migration, client API édité à la main) — l'export les transporte ensuite gratuitement — puis un placement dense par spans côté Unity. <strong>L'arc est conservé</strong> : ce n'est pas un défaut de rendu, un mur plat se regarde de biais sur ses bords. La grille est calculée en cellules, puis chaque cellule est <em>mappée</em> sur un angle et une hauteur. Cible <strong>6 colonnes × 2 rangées</strong> — le confort visuel en VR c'est ±60°, ce qui donne naturellement le format paysage proche du rendu web.</p>
|
|
||||||
|
|
||||||
<p>🟠 <strong>La notification annonce la mauvaise app.</strong> Modifier un contenu VR affiche « application mobile mise à jour ». Le paramètre existe déjà (<code>appUpdatedLabel</code>, <code>app_configuration_link_screen.dart:144</code>) et n'est simplement pas passé par <code>vr_screen.dart</code>. Une clé i18n et quatre lignes — <strong>~15 minutes</strong>.</p>
|
|
||||||
|
|
||||||
<p>🟠 <strong>La popup d'édition d'un casque est celle du kiosk.</strong> Largeur figée à 580, une colonne scrollée, et deux boutons dont les libellés « Annuler »/« Changer » sont <strong>en dur</strong>. Mais le vrai problème n'est pas la mise en page : <strong>la moitié des champs est morte en VR</strong> — couleur principale, couleur de fond, loader, pourcentage d'arrondis, place des sections, fond d'image, heure, date. <code>grep -r</code> sur <code>Assets/Scripts</code> : <strong>zéro occurrence</strong> de chacun. Un casque n'a ni écran de chargement 2D ni horloge d'accueil. À remplacer par une popup VR dédiée, paysage deux colonnes.</p>
|
|
||||||
|
|
||||||
<p>⚠️ <strong>Ce que ce chantier dit du reste</strong> : les quatre écarts ont la même cause — un écran partagé par quatre canaux qui n'ont pas les mêmes réglages. Le plan recommande d'<strong>en sortir</strong> pour la VR plutôt que d'y ajouter un quatrième cas particulier.</p>
|
|
||||||
|
|
||||||
<p><strong>Ordre</strong> : la notification (indépendante, 15 min) → la popup (indépendante) → les spans backend + manager → le rendu Unity (rien de visible avant). Quatre mini-prompts de lancement sont en fin de plan.</p>
|
|
||||||
@ -24,7 +24,6 @@ Plans détaillés, pas encore démarrés en dev (sauf mention contraire) :
|
|||||||
| AI Persona par venue (guide IA personnalisable) | [v2/myinfomate-ai-persona-analysis.md](../v2/myinfomate-ai-persona-analysis.md) | Architecture de référence pour les chantiers IA ci-dessus |
|
| AI Persona par venue (guide IA personnalisable) | [v2/myinfomate-ai-persona-analysis.md](../v2/myinfomate-ai-persona-analysis.md) | Architecture de référence pour les chantiers IA ci-dessus |
|
||||||
| Lunettes Ray-Ban Meta | [roadmap.md](../roadmap.md) (section XR), [rayban-meta-integration.md](../rayban-meta-integration.md) | 🔨 **POC fonctionnel — bien plus avancé que ce que cette ligne disait jusqu'au 2026-08-09.** Voir §5bis |
|
| Lunettes Ray-Ban Meta | [roadmap.md](../roadmap.md) (section XR), [rayban-meta-integration.md](../rayban-meta-integration.md) | 🔨 **POC fonctionnel — bien plus avancé que ce que cette ligne disait jusqu'au 2026-08-09.** Voir §5bis |
|
||||||
| VR Meta Quest (borne office tourisme) | [v2/vr-quest-unity-plan.md](../v2/vr-quest-unity-plan.md) · [roadmap.md](../roadmap.md) (section XR) | 📦 **V2** — ⚠️ **« zéro ligne de code » était faux** (corrigé le 2026-08-31, relevé dans le code). Le canal VR est déjà dans le modèle : `AppType.VR`, `Instance.IsVR` en base depuis juillet 2025, sous-menu « VR » branché dans `main_screen.dart:65`, filtre stats + i18n `statsChannelVR` prêts. **Ce qui manque** : l'écran de flotte (`main_screen.dart:704` = `Text("TODO vr")`), un `Device.AppType` (le `DeviceController` est hardcodé sur `Tablet`), et l'app Unity. Back-office XR : **8-12 j**. App Unity : **3-4 mois**. La landing annonce « Meta Quest — Bientôt » en 4 langues (`translations.ts`, clé `mode4Desc`) |
|
| VR Meta Quest (borne office tourisme) | [v2/vr-quest-unity-plan.md](../v2/vr-quest-unity-plan.md) · [roadmap.md](../roadmap.md) (section XR) | 📦 **V2** — ⚠️ **« zéro ligne de code » était faux** (corrigé le 2026-08-31, relevé dans le code). Le canal VR est déjà dans le modèle : `AppType.VR`, `Instance.IsVR` en base depuis juillet 2025, sous-menu « VR » branché dans `main_screen.dart:65`, filtre stats + i18n `statsChannelVR` prêts. **Ce qui manque** : l'écran de flotte (`main_screen.dart:704` = `Text("TODO vr")`), un `Device.AppType` (le `DeviceController` est hardcodé sur `Tablet`), et l'app Unity. Back-office XR : **8-12 j**. App Unity : **3-4 mois**. La landing annonce « Meta Quest — Bientôt » en 4 langues (`translations.ts`, clé `mode4Desc`) |
|
||||||
| Tags NFC (déclencheur de section) | [v2/nfc-triggers-plan.md](../v2/nfc-triggers-plan.md) | 📦 **V2** — analysé dans le code le 2026-09-16. Troisième déclencheur physique à côté du QR et du beacon. ⚠️ **Le coût n'est pas le NFC** : les **Universal Links / App Links n'existent pas** (aucun `assetlinks.json` ni `apple-app-site-association`, `app_links` branché seulement sur le retour Meta AI, `main.dart:69`) — ce lot-là **sert aussi au QR**, qui aujourd'hui n'ouvre pas l'app quand il est scanné par l'appareil photo natif. Le tag porte **la même URL que le QR** : aucune entité, aucun endpoint de résolution en v1. Dépend du **déploiement de `visitapp-web`** pour le fallback sans app. 4-7 j |
|
|
||||||
| Canal subsides | [plan-import-ia-stats-subsides.md](../plan-import-ia-stats-subsides.md) | Actions administratives/réseau, pas de dev |
|
| Canal subsides | [plan-import-ia-stats-subsides.md](../plan-import-ia-stats-subsides.md) | Actions administratives/réseau, pas de dev |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -9,67 +9,20 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Comment passer ce plan — ajouté le 2026-09-16
|
|
||||||
|
|
||||||
**Quatre séances, dans cet ordre.** Chacune est arrêtée par la précédente : inutile de chercher un bug
|
|
||||||
de narration si l'upload est cassé en dessous.
|
|
||||||
|
|
||||||
| Séance | Sections | Durée | Ce qu'un ❌ arrête |
|
|
||||||
|---|---|---|---|
|
|
||||||
| **1. Le socle** | § 0, § 0bis | ~45 min | Tout. Rien d'autre ne se teste |
|
|
||||||
| **2. L'argent et l'image** | § 1, § 2, § 2bis | ~1 h | Les § 3 à 5 (pas de génération fiable, pas de narration) |
|
|
||||||
| **3. La narration** | § 3, § 4 | ~1 h 30 | Les § 5 et 5bis côté visiteur |
|
|
||||||
| **4. Les trois fronts** | § 5, § 5bis, § 6 | ~1 h | Rien — c'est la fin |
|
|
||||||
|
|
||||||
**Comment reporter.** Un ✅ tout court suffit quand c'est conforme. Un ❌ demande trois choses et pas
|
|
||||||
une de plus : *ce que tu as fait*, *ce que tu as vu*, *ce que tu attendais*. Une capture vaut mieux
|
|
||||||
qu'une description pour tout ce qui est visuel. N'essaie pas de diagnostiquer — c'est mon travail, et
|
|
||||||
un diagnostic dans la colonne ✓ masque souvent le vrai symptôme.
|
|
||||||
|
|
||||||
**Ce qui n'est pas un bug** : la liste des limites connues en fin de document. À lire **avant** de
|
|
||||||
commencer, pas après avoir signalé l'une d'elles.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Prérequis — une fois
|
## 0. Prérequis — une fois
|
||||||
|
|
||||||
| # | Étape | Attendu | ✓ |
|
| # | Étape | Attendu | ✓ |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 0.1 | `winget install Gyan.FFmpeg`, rouvrir le terminal, `ffmpeg -version` | Une version s'affiche. Sans ffmpeg, la génération audio échoue et les crédits sont remboursés | ✅ 16/09 — 9.0.1-full_build, alias winget dans le Path persistant |
|
| 0.1 | `winget install Gyan.FFmpeg`, rouvrir le terminal, `ffmpeg -version` | Une version s'affiche. Sans ffmpeg, la génération audio échoue et les crédits sont remboursés | |
|
||||||
| 0.2 | `dotnet user-secrets set "AI:ApiKey" "<clé Gemini>" --project manager-service/ManagerService` | `dotnet user-secrets list` montre `AI:ApiKey` (et `Studio:Fal:Key`) | ✅ 16/09 — « Successfully saved AI:ApiKey » (Thomas) |
|
| 0.2 | `dotnet user-secrets set "AI:ApiKey" "<clé Gemini>" --project manager-service/ManagerService` | `dotnet user-secrets list` montre `AI:ApiKey` (et `Studio:Fal:Key`) | |
|
||||||
| 0.3 | Lancer Postgres puis `dotnet ef database update` avec `MIGRATIONS_CONNECTION` (voir la note mémoire EF) | Migrations appliquées : `AddAiWatermarkBurned`, `StudioCalibrationEngravingStyle`, `StudioCreditUnitPerReference`, `AddNarrators`, `AddGeoPointAudio`, `AddCasting` | ✅ 16/09 — les 6 appliquées en une passe, « Done. » |
|
| 0.3 | Lancer Postgres puis `dotnet ef database update` avec `MIGRATIONS_CONNECTION` (voir la note mémoire EF) | Migrations appliquées : `AddAiWatermarkBurned`, `StudioCalibrationEngravingStyle`, `StudioCreditUnitPerReference`, `AddNarrators`, `AddGeoPointAudio`, `AddCasting` | |
|
||||||
| 0.4 | `dotnet run` (manager-service), `flutter run -d chrome` (manager-app) | Les deux démarrent, connexion SuperAdmin OK | |
|
| 0.4 | `dotnet run` (manager-service), `flutter run -d chrome` (manager-app) | Les deux démarrent, connexion SuperAdmin OK | |
|
||||||
| 0.5 | **Dialogue d'instance** — il n'est pas dans un écran : **pied du menu de gauche → icône ⇄ « Changer d'instance »** (visible seulement en SuperAdmin) → dans la liste, le **crayon** « Configurer le plan » de la ligne voulue. Activer **Assistant IA**, puis **Studio**, puis accorder **100 crédits** | Solde 100 dans la jauge du menu | |
|
| 0.5 | Dialogue d'instance (SuperAdmin) : assistant **et** Studio activés, **100 crédits** accordés | Solde 100 dans la jauge du menu | |
|
||||||
| 0.5b | ⚠️ Dans ce même dialogue, **décocher Assistant IA** | L'interrupteur **Studio se désactive tout seul** et devient grisé : pas de Studio sans assistant (décision 12). Recocher les deux avant de continuer | |
|
|
||||||
| 0.6 | Guide IA › Personnages : un personnage **Léon** avec une **voix** (Umbriel) et une intonation, un second **La sentinelle** avec une autre voix (Kore) | Les deux apparaissent dans la liste, sans « (sans voix) » | |
|
| 0.6 | Guide IA › Personnages : un personnage **Léon** avec une **voix** (Umbriel) et une intonation, un second **La sentinelle** avec une autre voix (Kore) | Les deux apparaissent dans la liste, sans « (sans voix) » | |
|
||||||
| 0.7 | Donner un **portrait** à Léon (onglet Visage → générer ou choisir une vue Portrait) | La vue Portrait est enregistrée | |
|
| 0.7 | Donner un **portrait** à Léon (onglet Visage → générer ou choisir une vue Portrait) | La vue Portrait est enregistrée | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0bis. Le socle d'upload — ajouté le 2026-09-16
|
|
||||||
|
|
||||||
> **Pourquoi cette section n'existait pas, et pourquoi elle doit exister.** Le lot 0 a **réécrit tout le
|
|
||||||
> chemin d'upload** de manager-app : plus aucun appel au SDK Firebase, tout passe par une URL V4 signée
|
|
||||||
> puis `POST /ingest`. C'est codé depuis le 13/09 et couvert par des tests unitaires, mais **aucun
|
|
||||||
> fichier n'a jamais été déposé depuis un navigateur**. Les § 1 à 5 supposent tous que ça marche : la
|
|
||||||
> médiathèque reçoit les images générées, les MP3 de narration, les portraits. Tester la narration
|
|
||||||
> avant ce socle, c'est chercher une fuite au toit avant d'avoir regardé les fondations.
|
|
||||||
|
|
||||||
| # | Étape | Attendu | ✓ |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 0b.1 | Médiathèque : déposer une **image JPEG** de plus de 2560 px | Elle monte (barre de progression), apparaît dans la grille, s'ouvre. Réduite à 2560 px côté long | |
|
|
||||||
| 0b.2 | Déposer un **PNG à transparence** | Reste un PNG, l'alpha est préservé (le voir sur fond coloré) | |
|
|
||||||
| 0b.3 | Déposer un **MP3**, un **PDF** | Montent tels quels, lisibles / téléchargeables depuis la fiche | |
|
|
||||||
| 0b.4 | **Remplacer** le fichier d'une ressource existante | Le nouveau s'affiche partout où la ressource était utilisée, l'ancien ne traîne pas | |
|
|
||||||
| 0b.5 | **Supprimer** une ressource non utilisée | Disparaît de la grille ; recharger la page ne la ramène pas | |
|
|
||||||
| 0b.6 | ⚠️ Fichier **au-dessus du plafond de son type** (ex. une image > 30 Mo) | Refusé avec un message traduit, **pas** une erreur brute ni un échec muet. Tester en FR, EN **et** NL | |
|
|
||||||
| 0b.7 | ⚠️ **Sélecteur de ressource** : champ image d'une section → ajouter un fichier depuis la modale → Enregistrer | Le fichier arrive **et** le champ de la section le pointe. C'est le piège historique : la modale embarque `ResourcesScreen`, une régression ici touche les 13 types de section | |
|
|
||||||
| 0b.8 | Ouvrir une ressource déposée depuis **visitapp-web** et depuis **mymuseum-visitapp** | L'URL s'ouvre des deux côtés (le jeton de téléchargement est bien posé à l'écriture) | |
|
|
||||||
| 0b.9 | Remplacer l'image d'une visite **déjà téléchargée** dans mymuseum-visitapp | L'app la re-télécharge, n'affiche pas l'ancienne | |
|
|
||||||
| 0b.10 | Sélecteur de langues d'une configuration | **Le luxembourgeois (LB) est proposé**, avec son drapeau (ajouté au lot 0, jamais vu à l'écran) | |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Mention IA dans le fichier (lot 6)
|
## 1. Mention IA dans le fichier (lot 6)
|
||||||
|
|
||||||
| # | Étape | Attendu | ✓ |
|
| # | Étape | Attendu | ✓ |
|
||||||
@ -96,25 +49,6 @@ commencer, pas après avoir signalé l'une d'elles.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2bis. Les crédits, pour de vrai — ajouté le 2026-09-16
|
|
||||||
|
|
||||||
> Le § 2 vérifie que **l'estimation** affichée est juste. Cette section-ci vérifie que **le solde**
|
|
||||||
> bouge juste, ce qui n'est pas la même chose : le mécanisme est une réservation puis un débit réel,
|
|
||||||
> avec remboursement en cas d'échec, et rien de tout ça n'a tourné en conditions réelles.
|
|
||||||
|
|
||||||
| # | Étape | Attendu | ✓ |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 2b.1 | Noter le solde, lancer une génération de 3 variantes, **regarder la jauge pendant** le travail | Le solde baisse **dès le lancement** (réservation), pas à l'arrivée des images | |
|
|
||||||
| 2b.2 | À la fin | Le débit définitif égale l'estimation annoncée, ni plus ni moins | |
|
|
||||||
| 2b.3 | Fermer le panneau **sans valider** aucune variante | Les crédits restent dépensés (l'appel a coûté), mais **rien** ne s'ajoute au quota de stockage | |
|
|
||||||
| 2b.4 | Lancer **deux générations coup sur coup** sans attendre la première | Les deux réservations se cumulent ; le solde ne descend jamais sous zéro | |
|
|
||||||
| 2b.5 | Solde insuffisant pour 3 variantes mais suffisant pour 1 | Refus **avant** l'appel, message clair, aucun crédit pris | |
|
|
||||||
| 2b.6 | ⚠️ Génération qui échoue (couper le réseau du serveur vers fal, ou fausse clé) | Le solde **revient** à sa valeur d'avant. C'est le point le plus important de la section | |
|
|
||||||
| 2b.7 | Abonnement › journal des crédits | Une ligne par mouvement (réservation, débit, remboursement), export CSV lisible | |
|
|
||||||
| 2b.8 | SuperAdmin : accorder 50 crédits à l'instance | Solde +50, et la **date d'expiration repoussée à +12 mois sur tout le solde** (pas seulement sur les 50) | |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Qui raconte quoi (lot 8a et 8c)
|
## 3. Qui raconte quoi (lot 8a et 8c)
|
||||||
|
|
||||||
Préparer une configuration avec un **parcours de 4 étapes** (titre + description remplis en FR et NL) et un **article** avec du texte.
|
Préparer une configuration avec un **parcours de 4 étapes** (titre + description remplis en FR et NL) et un **article** avec du texte.
|
||||||
@ -225,23 +159,3 @@ ajouter **La cuisinière sans portrait** comme narratrice d'une étape).
|
|||||||
- Hors ligne, mymuseum-visitapp ne montre pas le portrait.
|
- Hors ligne, mymuseum-visitapp ne montre pas le portrait.
|
||||||
- Le portrait affiché est le portrait **actuel** du personnage ; le nom et la voix sont ceux de l'audio entendu.
|
- Le portrait affiché est le portrait **actuel** du personnage ; le nom et la voix sont ceux de l'audio entendu.
|
||||||
- Les extraits d'écoute des voix (lot 7) ne sont pas encore produits.
|
- Les extraits d'écoute des voix (lot 7) ne sont pas encore produits.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ce que ce plan ne couvre **pas** — ajouté le 2026-09-16
|
|
||||||
|
|
||||||
À savoir avant de conclure « le Studio est testé » :
|
|
||||||
|
|
||||||
- **L'infra du bucket de prod.** Le lot 0 est codé mais son volet infra reste ouvert : CORS des origines
|
|
||||||
de prod, cycle de vie `incoming/`, et surtout **fermeture des règles Storage en écriture** — qui doit
|
|
||||||
se faire *après* le déploiement du nouveau manager-app, sinon l'upload de la version encore en ligne
|
|
||||||
casse. Rien de tout ça ne se voit depuis un poste de dev.
|
|
||||||
- **Le plafond journalier par utilisateur** : il *est* configurable, champ « Plafond par utilisateur et
|
|
||||||
par jour (crédits, 0 = aucun) » dans le dialogue d'instance, sous l'interrupteur Studio. Il vaut 0 par
|
|
||||||
défaut, donc rien ne se déclenche tant qu'on n'y met pas une valeur — à tester le jour où on veut
|
|
||||||
vérifier le garde-fou « stagiaire », pas dans cette passe.
|
|
||||||
- **Le webhook fal** en conditions réelles : en local `Studio:WebhookBaseUrl` est vide, la relecture se
|
|
||||||
fait par sondage. La vérification de signature ED25519 ne sera exercée qu'en préprod.
|
|
||||||
- **La charge** : tout ce plan se passe à un seul utilisateur. Le sémaphore à 2 images simultanées de
|
|
||||||
l'ingestion ne se voit pas à cette échelle.
|
|
||||||
- **Les lots 9 à 11** (avant/après, vidéo, 3D) : pas commencés.
|
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
# Tags NFC comme déclencheurs de section
|
|
||||||
|
|
||||||
> Chantier **V2**, **analysé dans le code le 2026-09-16** à partir d'une spec d'intention.
|
|
||||||
> Carte kanban : `cards/5-planifie/265-…`
|
|
||||||
> Ce document **corrige la spec d'origine sur deux points de réalité** (le mot « POI », et
|
|
||||||
> le schéma d'URL avec table de mapping), puis dit comment on le fait.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ce que la spec d'origine supposait, et ce que dit le code
|
|
||||||
|
|
||||||
| Hypothèse de la spec | Réalité relevée |
|
|
||||||
|---|---|
|
|
||||||
| « Déclencher un **POI** » | Dans MyInfoMate, un **POI est un point posé sur un GLB** (3D/VR — voir `immersif-frontiere-plan.md` §9). Le déclencheur physique d'un contenu, c'est une **`Section`** : `Section.IsBeacon` / `Section.BeaconId` (`Section.cs:57`), lue côté visiteur par `geo_beacon_trigger_service.dart:177` et `configuration_page.dart:205`. Le NFC se branche **au même endroit que le beacon**, pas ailleurs |
|
|
||||||
| « Réutiliser l'endpoint de résolution des QR, s'ils utilisent déjà une URL de ce format » | **Il n'y en a aucun.** Les QR portent l'id de section **directement dans l'URL**, et trois formats coexistent sur le terrain (`ScannerDialog.dart:31-38`) : `app.myinfomate.be/download/{instanceId}/{configId}/{sectionId}`, `web.mymuseum.be/{instanceId}/{configId}/{sectionId}` (déjà imprimé, MDLF), et un **id de section brut** (déjà imprimé, Fort Saint-Héribert). Il n'y a donc rien à réutiliser : l'entité « tag », le code opaque et l'endpoint `code → section` seraient **du neuf intégral** |
|
|
||||||
| « Déclarer les URLs en Universal Links / App Links » | **Rien n'existe.** Aucun `assetlinks.json` ni `apple-app-site-association` sur nos domaines (le seul du workspace est dans `OpenGlasses`, qui n'est pas à nous). `app_links` est bien au `pubspec.yaml:89` mais branché **uniquement sur le retour de deep link Meta AI** (`main.dart:69` → `MetaGlassesService.handleDeepLink`), et `AndroidManifest.xml` n'a pas d'intent-filter `autoVerify` sur un domaine http |
|
|
||||||
| « Si l'app n'est pas installée, l'URL affiche une page web utile » | `visitapp-web` fait exactement ça (`app.myinfomate.be/[slug]`), mais **il n'est pas déployé** — il manque le Dockerfile et l'entrée dans le compose (carte « Déployer le visiteur web »). Le fallback est une **dépendance**, pas un acquis |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Décisions
|
|
||||||
|
|
||||||
| Décision | Conséquence |
|
|
||||||
|---|---|
|
|
||||||
| **1. Le tag porte la même URL que le QR** | Pas d'entité `NfcTag`, pas de code opaque, pas d'endpoint de résolution, pas de CRUD manager. Le mapping tag → section, c'est l'URL écrite sur le tag. **Zéro ligne de backend pour la v1** |
|
|
||||||
| **2. L'indirection `code → section` est reportée, pas refusée** | L'argument « réassigner sans réécrire le tag » est réel, mais il vaut **beaucoup moins en NFC qu'en QR** : réécrire un NTAG prend deux secondes avec un téléphone, alors qu'un QR se réimprime, se replastifie et se recolle. On ouvrira l'indirection le jour où un client gère un parc de tags — et ce jour-là elle devra couvrir **QR et NFC ensemble**, pas le NFC seul |
|
|
||||||
| **3. Le lot utile est celui des liens, pas celui du NFC** | Universal Links + App Links + routage du deep link entrant vers la section. ✅ **Le QR en bénéficie autant** : aujourd'hui un QR scanné par l'appareil photo natif n'ouvre pas l'app, il ouvre le navigateur. C'est ce lot qui porte la valeur, le NFC n'en est qu'un second émetteur |
|
|
||||||
| **4. Lecture in-app en fallback seulement** | `nfc_manager` derrière le bouton Scanner existant (`ScannerBouton.dart`), à côté du QR. Le cas nominal reste **le tag lu sans ouvrir l'app** — c'est tout l'intérêt du canal |
|
|
||||||
| **5. Écran de programmation des tags : Android-only** | L'écriture NDEF depuis iOS est trop contrainte pour un outil de terrain, et le public visé est **l'admin qui pose les tags**, pas le visiteur. Un seul appareil de programmation suffit à un lieu |
|
|
||||||
| **6. Verrouillage en écriture : hors v1** | Irréversible, donc une erreur de pose coûte le tag. À ouvrir après un vrai déploiement, avec double confirmation |
|
|
||||||
| **7. Pas de lecture par UID** | Confirmé : l'UID n'est pas réassignable et impose une table de correspondance — donc tous les inconvénients de la décision 2 sans son bénéfice |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ce qui ne marchera pas — à savoir avant de le vendre
|
|
||||||
|
|
||||||
- **Aucune lecture NFC sur `visitapp-web`.** WebNFC est Chrome/Android uniquement, jamais Safari. Le plan **Essentiel étant web-only**, le NFC ne s'y vend pas comme « scan dans l'app » — il s'y vend comme « on approche le téléphone, une page s'ouvre », ce qui est vrai et ne demande rien de plus que la décision 1.
|
|
||||||
- **iPhone 7 / 8 / X** ne lisent pas les tags en arrière-plan : il leur faut l'app ouverte. iPhone **XS et plus** lisent un NDEF sans aucune app installée — c'est l'argument commercial réel, et il est bon.
|
|
||||||
- **Un tag à plat sur du métal ne se lit pas.** Prévoir des tags sur mousse ou des NTAG « on-metal » ; le raccourci du bois transparent au BLE (carte beacons) n'a pas d'équivalent ici.
|
|
||||||
- **Le NFC ne remplace pas le beacon** : il demande un geste volontaire à 1-2 cm, il ne déclenche rien à l'approche. Les trois déclencheurs se cumulent, ils ne se substituent pas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Découpage
|
|
||||||
|
|
||||||
| Lot | Contenu | Estimation |
|
|
||||||
|---|---|---|
|
|
||||||
| **A — Liens universels** (le seul indispensable) | `assetlinks.json` + `apple-app-site-association` servis sur le domaine, intent-filter `autoVerify` + capability Associated Domains, routage du deep link entrant vers la section (logique partagée avec le parsing de `ScannerDialog`, à extraire du dialogue). **Bénéficie au QR dès la livraison** | 2-3 j |
|
|
||||||
| **B — NFC** | Permission et intent-filter NDEF Android, capability NFC Tag Reading + `NFCReaderUsageDescription` + entitlement iOS, `nfc_manager` derrière le bouton Scanner | 1-2 j |
|
|
||||||
| **C — Écran de programmation admin** | Choisir une section → approcher un tag → écrire l'URL NDEF. Android-only, NTAG213/215/216 | 1-2 j |
|
|
||||||
| **D — Fallback web** | Rien à écrire : c'est le déploiement de `visitapp-web`. **Dépendance externe** | — |
|
|
||||||
| *(reporté)* **E — Indirection `code → section`** | Entité, CRUD manager, endpoint de résolution — et alors **pour QR et NFC ensemble** | 3-4 j |
|
|
||||||
|
|
||||||
**Total v1 (A+B+C) : 4-7 jours**, dont A porte à lui seul la moitié de la valeur et sert déjà au QR.
|
|
||||||
@ -161,8 +161,6 @@ Tout sur `Instance`, dupliqué depuis `SubscriptionPlan` : `StorageQuotaBytes`,
|
|||||||
| 22 | Plafond dur et dotation de plan — *13/09, lot 1* | **Retirés.** Avec des crédits prépayés (décision 17), le solde *est* le plafond : un « plafond dur organisation » n'a plus rien à borner. Seul reste `StudioPerUserDailyCap`, en crédits. `SubscriptionPlan.HasStudio` / `StudioCreditsGranted` et `Instance.StudioProviderRegion` ne sont pas créés tant que rien ne les lit (prix différés, décision 16 ; région déclarative) |
|
| 22 | Plafond dur et dotation de plan — *13/09, lot 1* | **Retirés.** Avec des crédits prépayés (décision 17), le solde *est* le plafond : un « plafond dur organisation » n'a plus rien à borner. Seul reste `StudioPerUserDailyCap`, en crédits. `SubscriptionPlan.HasStudio` / `StudioCreditsGranted` et `Instance.StudioProviderRegion` ne sont pas créés tant que rien ne les lit (prix différés, décision 16 ; région déclarative) |
|
||||||
| 23 | Écarts des lots 2 à 4 — *13/09* | **Fournisseur interchangeable** : `IGenerationProvider` (soumission, sondage, lecture du webhook) + `GenerationModel.ProviderKey` en base ; changer de fournisseur = une classe et une ligne de catalogue. **Une requête par variante** (FLUX.2 pro n'a pas de `num_images`). **Référence source avant celles de l'identité** dans l'ordre d'envoi. Migration unique `AddStudioGeneration` pour les lots 2 et 3. **Non livrés** : `preview`, `GET /jobs`, `cancel`, `reject`, `regenerate`, l'interrupteur UI de `CanValidateAssets`. **Pas de `target` serveur** : l'onglet « Générer » vit dans le sélecteur (`showSelectResourceModal`, mode sélection) et la ressource validée revient au champ qui l'a ouvert — donc absent du bouton « Ajouter » de la Médiathèque, qui relève du lot 5. Surcharges d'identité en puces, pas en onglets ; `ModelKey` non exposé (un seul modèle). **`GuidedStep.ImageUrl` conservée** et remplie par le client avec `ImageResourceId` (motif `GeoPoint`) : visitapp-web n'a rien à changer, mymuseum-visitapp lit l'id pour l'hors-ligne, tablet-app n'a pas d'étapes. `CreditCost = 10` provisoire jusqu'au calibrage || 24 | Modèles 3D et multi-vues — *15/09* | **Tout reste sur fal** : Meshy (v5, v6-preview, v7), Hi3D, Tripo3D, Hunyuan3D et Trellis y sont exposés — aucun second fournisseur à intégrer, aucune seconde clé. **Deux lignes de catalogue, pas une** : `3d-default` mono-image (~0,02–0,05 $) et `3d-multiview` = `meshy/v7/multi-image-to-3d` (~1,20 $). Le facteur ~25 sur le coût impose que le basculement de l'un à l'autre soit **annoncé avant la génération, jamais silencieux**. Hi3D n'expose pas de multiview : excellent en fidélité structurelle, il ne couvre pas le cas d'usage central. Ids et prix à revérifier au calibrage (§9.4) |
|
| 23 | Écarts des lots 2 à 4 — *13/09* | **Fournisseur interchangeable** : `IGenerationProvider` (soumission, sondage, lecture du webhook) + `GenerationModel.ProviderKey` en base ; changer de fournisseur = une classe et une ligne de catalogue. **Une requête par variante** (FLUX.2 pro n'a pas de `num_images`). **Référence source avant celles de l'identité** dans l'ordre d'envoi. Migration unique `AddStudioGeneration` pour les lots 2 et 3. **Non livrés** : `preview`, `GET /jobs`, `cancel`, `reject`, `regenerate`, l'interrupteur UI de `CanValidateAssets`. **Pas de `target` serveur** : l'onglet « Générer » vit dans le sélecteur (`showSelectResourceModal`, mode sélection) et la ressource validée revient au champ qui l'a ouvert — donc absent du bouton « Ajouter » de la Médiathèque, qui relève du lot 5. Surcharges d'identité en puces, pas en onglets ; `ModelKey` non exposé (un seul modèle). **`GuidedStep.ImageUrl` conservée** et remplie par le client avec `ImageResourceId` (motif `GeoPoint`) : visitapp-web n'a rien à changer, mymuseum-visitapp lit l'id pour l'hors-ligne, tablet-app n'a pas d'étapes. `CreditCost = 10` provisoire jusqu'au calibrage || 24 | Modèles 3D et multi-vues — *15/09* | **Tout reste sur fal** : Meshy (v5, v6-preview, v7), Hi3D, Tripo3D, Hunyuan3D et Trellis y sont exposés — aucun second fournisseur à intégrer, aucune seconde clé. **Deux lignes de catalogue, pas une** : `3d-default` mono-image (~0,02–0,05 $) et `3d-multiview` = `meshy/v7/multi-image-to-3d` (~1,20 $). Le facteur ~25 sur le coût impose que le basculement de l'un à l'autre soit **annoncé avant la génération, jamais silencieux**. Hi3D n'expose pas de multiview : excellent en fidélité structurelle, il ne couvre pas le cas d'usage central. Ids et prix à revérifier au calibrage (§9.4) |
|
||||||
| 25 | Casting visiteur — *15/09* | **Retenu** — lève le « hors périmètre » de §3.8. `ConfigurationPersona { ConfigurationId, PersonaId, Order, IsFeatured }` ne porte **que** l'ordre et la mise en avant : l'appartenance reste **calculée** depuis les narrateurs assignés. Un personnage ne s'assigne qu'à **un seul endroit — le contenu** ; l'écran casting est une *vue*, jamais une seconde saisie. Rien ne s'affiche côté visiteur sans `Configuration.IsCastingShownToVisitor` (défaut off). Lot 8bis |
|
| 25 | Casting visiteur — *15/09* | **Retenu** — lève le « hors périmètre » de §3.8. `ConfigurationPersona { ConfigurationId, PersonaId, Order, IsFeatured }` ne porte **que** l'ordre et la mise en avant : l'appartenance reste **calculée** depuis les narrateurs assignés. Un personnage ne s'assigne qu'à **un seul endroit — le contenu** ; l'écran casting est une *vue*, jamais une seconde saisie. Rien ne s'affiche côté visiteur sans `Configuration.IsCastingShownToVisitor` (défaut off). Lot 8bis |
|
||||||
| 26 | Prise de vue mobile — *16/09* | **Page de dépôt dédiée ouverte par QR code, pas un manager-app responsive.** Le conservateur est devant la vitrine avec son téléphone, pas devant son poste ; rendre responsive un back-office Flutter web pour un seul écran revient à le rendre responsive tout court — refus déjà posé sur le routing (décision 23) — et CanvasKit se charge mal sur la 4G d'une salle. La page vit sur **la même surface web non-Flutter que l'éditeur de POI** (décision n°9 d'[immersif-frontiere-plan.md](immersif-frontiere-plan.md)) : aucune nouvelle cible de déploiement. Capture par `<input type="file" accept="image/*" capture="environment">`, qui ouvre l'appareil photo natif — **jamais `getUserMedia`**, dont le flux est sans autofocus ni traitement constructeur, alors que c'est le seul gabarit du Studio où la qualité des photos pèse plus lourd que le prompt. Accès par **token de dépôt** borné (§3.7bis), jamais un JWT manager. **Lot 11bis, après le lot 11** : le dépôt de fichiers depuis le poste couvre déjà le besoin fonctionnel |
|
|
||||||
| 27 | Alerte de solde, pas de nouveau plafond — *16/09* | **Le plafond reste retiré** (décision 22) : avec des crédits prépayés, le solde *est* le plafond, et `StudioPerUserDailyCap` couvre déjà le budget vidé en une après-midi. Ce qui manque est le **préavis** — le modèle n'est pas celui de Google AI Studio ni de fal.ai, **postpayés**, où l'alerte protège d'une facture ; ici elle protège d'une **indisponibilité**. On ajoute `Instance.StudioCreditsAlertThreshold` (0 = désactivé) + `StudioCreditsAlertSentAt` (anti-répétition), déclenchés dans `StudioCreditService.Record` au franchissement du seuil et dans `ExpireDueAsync` pour un rappel **d'expiration à J-30** — c'est le vrai risque de la décision 17, un solde qui s'évapore en silence chez une institution publique. Envoi par `IEmailService` (Resend, déjà en place), champ dans l'écran Facturation et bandeau dans le popover de la jauge. **Après la décision 16** : un seuil en crédits n'a pas de valeur par défaut tant que l'unité n'est pas fixée |
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -733,39 +731,6 @@ publiques exposées en JWKS :
|
|||||||
répond `200` sans rien refaire. D'où le téléchargement des octets dans un job, pas dans la requête ;
|
répond `200` sans rien refaire. D'où le téléchargement des octets dans un job, pas dans la requête ;
|
||||||
- corps : `{ request_id, gateway_request_id, status: "OK" | "ERROR", payload | error }`.
|
- corps : `{ request_id, gateway_request_id, status: "OK" | "ERROR", payload | error }`.
|
||||||
|
|
||||||
### 3.7bis Token de dépôt — le seul chemin d'écriture non authentifié
|
|
||||||
|
|
||||||
Réservé au lot 11bis (décision 26). À lire avant de l'écrire : ce serait **le seul endroit de la
|
|
||||||
solution où un client sans session peut faire écrire dans le bucket**, et les règles Storage de prod
|
|
||||||
sont ouvertes en écriture depuis 2024. Un token laxiste transformerait le QR en hébergeur de fichiers
|
|
||||||
gratuit pour qui le photographie par-dessus l'épaule.
|
|
||||||
|
|
||||||
```
|
|
||||||
StudioSourceToken
|
|
||||||
Token -- aléatoire 32 octets, indexé, jamais dérivé d'un id
|
|
||||||
JobDraftId -- le brouillon de génération 3D, et lui seul
|
|
||||||
InstanceId
|
|
||||||
SlotsLeft -- 4 au départ, décrémenté à chaque URL délivrée
|
|
||||||
ExpiresAt -- maintenant + 30 min : la durée d'une prise de vue
|
|
||||||
CreatedBy -- l'utilisateur qui a affiché le QR, pour le journal
|
|
||||||
```
|
|
||||||
|
|
||||||
Quatre bornes, toutes côté serveur :
|
|
||||||
|
|
||||||
- **portée d'un seul brouillon** — il n'autorise que le dépôt dans
|
|
||||||
`studio-sources/{instanceId}/{draftId}/{slot}`, aucune lecture, aucun autre chemin ;
|
|
||||||
- **durée courte** — 30 min, cohérent avec les 15 min des URLs signées de la décision 14 ;
|
|
||||||
- **compteur borné** — 4 URLs délivrées, puis le token est mort, même avant expiration ;
|
|
||||||
- **il ne signe rien lui-même** — il ouvre `POST /api/Studio/source-upload-url`, qui applique le
|
|
||||||
plafond de taille (`x-goog-content-length-range`) et le quota exactement comme le flux authentifié.
|
|
||||||
Le token est une autorisation, pas une capacité.
|
|
||||||
|
|
||||||
**Les photos sources ne sont pas des `Resource`.** Ce sont des entrées de génération : elles vivent
|
|
||||||
sous `studio-sources/`, hors quota de médiathèque, et tombent avec une règle de cycle de vie comme
|
|
||||||
celle posée sur `incoming/` au lot 0. Elles ne passent donc **pas** par `ResourceIngestionService` —
|
|
||||||
ce qui veut dire que **le lot 0, déjà codé le 13/09, n'est pas à rouvrir**. Seul le GLB produit
|
|
||||||
devient une `Resource`.
|
|
||||||
|
|
||||||
### 3.8 Personnages — fusion des trois plans, arrêtée le 2026-09-01
|
### 3.8 Personnages — fusion des trois plans, arrêtée le 2026-09-01
|
||||||
|
|
||||||
> **Trois plans décrivaient le même objet sans se croiser.** Le canon visuel (ce plan), les 3 frames
|
> **Trois plans décrivaient le même objet sans se croiser.** Le canon visuel (ce plan), les 3 frames
|
||||||
@ -1561,10 +1526,6 @@ l'écran doit être construit autour de ça.
|
|||||||
Chaque case porte une silhouette d'exemple. Sous le bloc, **une seule consigne**, courte :
|
Chaque case porte une silhouette d'exemple. Sous le bloc, **une seule consigne**, courte :
|
||||||
*même objet, même lumière, fond neutre, objet entier dans le cadre.* C'est la recommandation du
|
*même objet, même lumière, fond neutre, objet entier dans le cadre.* C'est la recommandation du
|
||||||
modèle, et elle pèse plus lourd que n'importe quel réglage.
|
modèle, et elle pèse plus lourd que n'importe quel réglage.
|
||||||
|
|
||||||
Chaque case accepte un fichier **ou** une photo prise sur place (lot 11bis). ⚠️ **La grille de
|
|
||||||
quatre est aussi un plafond, pas une mise en forme** : `meshy/v7/multi-image-to-3d` prend **2 à 4
|
|
||||||
vues**, une cinquième fait rejeter l'appel.
|
|
||||||
3. **Le coût se met à jour en direct, et le changement de modèle est annoncé** : 1 photo → modèle
|
3. **Le coût se met à jour en direct, et le changement de modèle est annoncé** : 1 photo → modèle
|
||||||
mono-image ; dès la 2ᵉ → Meshy v7 multi, **~25× plus cher**. Une ligne explicite au moment où
|
mono-image ; dès la 2ᵉ → Meshy v7 multi, **~25× plus cher**. Une ligne explicite au moment où
|
||||||
la bascule se produit, jamais une facture découverte après coup (même principe que le coût de
|
la bascule se produit, jamais une facture découverte après coup (même principe que le coût de
|
||||||
@ -1572,22 +1533,6 @@ l'écran doit être construit autour de ça.
|
|||||||
4. À l'inverse, **ne pas culpabiliser le mono-image** : une seule photo reste un usage légitime et
|
4. À l'inverse, **ne pas culpabiliser le mono-image** : une seule photo reste un usage légitime et
|
||||||
bon marché pour une illustration. La formulation est « plus de vues = plus fidèle », pas
|
bon marché pour une illustration. La formulation est « plus de vues = plus fidèle », pas
|
||||||
« vous avez mal fait ».
|
« vous avez mal fait ».
|
||||||
4bis. **Les N photos sources ne touchent pas `GenerationJob.SourceResourceId`.** Vérifié dans le code le
|
|
||||||
16/09 : cette colonne est **singulière** et porte un id de `Resource`
|
|
||||||
(`AddStudioGeneration:47`, `StudioGenerationService.cs:238`) — l'élargir en liste rouvrirait les lots 3
|
|
||||||
et 4, leurs tests et le chemin image qui fonctionne. À la place, une **table fille** dans la migration
|
|
||||||
du lot 11 :
|
|
||||||
|
|
||||||
```
|
|
||||||
StudioSourceFile
|
|
||||||
JobDraftId -- le brouillon de génération
|
|
||||||
Slot -- Face | Profil | Dos | TroisQuarts
|
|
||||||
StoragePath -- studio-sources/{instanceId}/{draftId}/{slot}
|
|
||||||
```
|
|
||||||
|
|
||||||
`SourceResourceId` reste intact pour le chemin image. C'est le pendant en entrée du multi-fichier en
|
|
||||||
sortie, et ça suit la décision 21, qui réserve déjà le job composite à la 3D.
|
|
||||||
|
|
||||||
5. Génération asynchrone en **job composite** (parent + enfants, décision 21), puis **normalisation
|
5. Génération asynchrone en **job composite** (parent + enfants, décision 21), puis **normalisation
|
||||||
canonique du GLB** à l'ingestion — échelle, pivot, orientation (décision n°2 d'
|
canonique du GLB** à l'ingestion — échelle, pivot, orientation (décision n°2 d'
|
||||||
`immersif-frontiere-plan` §5) : sans elle les POI posés sur un modèle sautent à la
|
`immersif-frontiere-plan` §5) : sans elle les POI posés sur un modèle sautent à la
|
||||||
@ -1598,33 +1543,6 @@ l'écran doit être construit autour de ça.
|
|||||||
7. La ressource validée revient au champ qui a ouvert le sélecteur, et les **POI se posent dans
|
7. La ressource validée revient au champ qui a ouvert le sélecteur, et les **POI se posent dans
|
||||||
l'éditeur three.js en iframe** (décision n°9) — le vrai morceau non trivial du chantier.
|
l'éditeur three.js en iframe** (décision n°9) — le vrai morceau non trivial du chantier.
|
||||||
|
|
||||||
### Lot 11bis — Prise de vue mobile par QR *(petit, juste après le lot 11)*
|
|
||||||
|
|
||||||
Décision 26. Le lot 11 suppose que le conservateur a déjà ses quatre JPEG sur son disque ; devant une
|
|
||||||
vitrine, il a un téléphone. Ce lot ferme l'écart **sans toucher au responsive de manager-app**.
|
|
||||||
|
|
||||||
1. Bouton « **Photographier avec mon téléphone** » à côté de la grille des quatre emplacements. Il
|
|
||||||
crée un `StudioSourceToken` (§3.7bis) et affiche le QR de l'URL de dépôt.
|
|
||||||
2. **Page de dépôt** — même surface web non-Flutter que l'éditeur de POI, route
|
|
||||||
`/studio/capture/{token}`. Quatre cases (Face / Profil / Dos / 3/4), la même consigne unique que
|
|
||||||
sur le poste, un `<input capture="environment">` par case. Pas de session, pas de routing, pas
|
|
||||||
d'état : demander l'URL signée, `PUT`, afficher la vignette.
|
|
||||||
3. **Le poste sert d'écran de contrôle** : les vignettes se remplissent en direct pendant la prise de
|
|
||||||
vue, par le **polling 2 s déjà retenu au §3.7** — aucun SSE à introduire pour ça.
|
|
||||||
4. Token expiré ou épuisé → la page le dit et renvoie vers le poste pour régénérer le QR. Le bouton
|
|
||||||
« Générer » reste sur le poste : **le téléphone dépose, il ne dépense pas de crédits.**
|
|
||||||
|
|
||||||
⚠️ **Le réseau est le point faible, pas la photo.** Une salle d'exposition en sous-sol coupe la 4G,
|
|
||||||
et une page web sans service worker perd les clichés déjà pris. Acceptable ici — l'utilisateur est un
|
|
||||||
conservateur qui recommence, pas un visiteur — mais c'est *cet* argument qui ferait revenir un jour
|
|
||||||
l'idée d'une application native, jamais la qualité d'image.
|
|
||||||
|
|
||||||
**Ne pas inverser l'ordre avec le lot 11.** « Je photographie, je m'envoie les photos, j'uploade
|
|
||||||
depuis le poste » coûte zéro et marche dès le premier jour. La valeur du chantier est dans la qualité
|
|
||||||
des quatre vues, l'annonce du basculement mono→multi à ×25 et la normalisation canonique du GLB.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
⚠️ **Ce lot faisait quatre lignes et se contentait de renvoyer au plan VR, qui lui-même renvoyait
|
⚠️ **Ce lot faisait quatre lignes et se contentait de renvoyer au plan VR, qui lui-même renvoyait
|
||||||
ici.** La frontière est tranchée depuis le 2026-09-11 dans
|
ici.** La frontière est tranchée depuis le 2026-09-11 dans
|
||||||
[immersif-frontiere-plan.md](immersif-frontiere-plan.md), qui ajoute : `Scene3D` comme
|
[immersif-frontiere-plan.md](immersif-frontiere-plan.md), qui ajoute : `Scene3D` comme
|
||||||
@ -1674,9 +1592,6 @@ la première migration du Studio, pas en arrivant au lot 11.
|
|||||||
| « Tout par le serveur » (première version de la décision 14) | `MemoryBufferThreshold = int.MaxValue` (`Startup.cs:112`) tient tout `IFormFile` en RAM, et une vidéo 360 de 1 Go traverserait le VPS | Abandonné le jour même : les octets vont directement chez Google |
|
| « Tout par le serveur » (première version de la décision 14) | `MemoryBufferThreshold = int.MaxValue` (`Startup.cs:112`) tient tout `IFormFile` en RAM, et une vidéo 360 de 1 Go traverserait le VPS | Abandonné le jour même : les octets vont directement chez Google |
|
||||||
| Objet écrit par le SDK GCS | Pas d'URL de téléchargement Firebase sans la métadonnée `firebaseStorageDownloadTokens` | Jeton posé à l'écriture, URL construite (§5) |
|
| Objet écrit par le SDK GCS | Pas d'URL de téléchargement Firebase sans la métadonnée `firebaseStorageDownloadTokens` | Jeton posé à l'écriture, URL construite (§5) |
|
||||||
| Trois décisions d'`immersif-frontiere-plan` §5 « à écrire avant la première migration » | Seule la n°5 (crédits) avait été reportée | N°1, 4 et 6 reportées le 13/09 : décisions 14, 20, 21 |
|
| Trois décisions d'`immersif-frontiere-plan` §5 « à écrire avant la première migration » | Seule la n°5 (crédits) avait été reportée | N°1, 4 et 6 reportées le 13/09 : décisions 14, 20, 21 |
|
||||||
| « Rendre manager-app responsive pour photographier l'objet » | Back-office Flutter web : le responsive d'un seul écran entraîne celui du routing, déjà exclu du périmètre (décision 23) — et CanvasKit à charger sur la 4G d'une salle pour faire un `<input type="file">` | Page de dépôt dédiée ouverte par QR, sur la surface web de l'éditeur de POI (décision 26, lot 11bis) |
|
|
||||||
| « Capture par `getUserMedia` » | Flux vidéo sans autofocus ni traitement constructeur, et manager-app n'a aucun binding caméra JS-interop | `<input type="file" accept="image/*" capture="environment">` : appareil photo natif, JPEG pleine résolution, retombe sur le sélecteur de fichiers sur poste |
|
|
||||||
| « Les photos sources passent par l'ingestion du lot 0 » | Le lot 0 est **codé depuis le 13/09**, et une photo source n'est pas une `Resource` : c'est une entrée de génération | `studio-sources/`, hors quota médiathèque, balayé par le cycle de vie. Lot 0 non rouvert |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -1747,13 +1662,6 @@ la première migration du Studio, pas en arrivant au lot 11.
|
|||||||
`x-goog-content-length-range` — sans quoi le navigateur refuse l'envoi.
|
`x-goog-content-length-range` — sans quoi le navigateur refuse l'envoi.
|
||||||
- **Cycle de vie** : règle `age: 1` sur le préfixe `incoming/` — un envoi jamais ingéré disparaît seul,
|
- **Cycle de vie** : règle `age: 1` sur le préfixe `incoming/` — un envoi jamais ingéré disparaît seul,
|
||||||
sans job Hangfire. S'ajoute à la règle des versions non courantes posée le 07/09.
|
sans job Hangfire. S'ajoute à la règle des versions non courantes posée le 07/09.
|
||||||
- **Cycle de vie à prévoir dans le même geste** : règle équivalente (`age: 7`) sur `studio-sources/`, le
|
|
||||||
préfixe des photos sources de la 3D (lot 11bis, §3.7bis). Rien ne les balaie autrement : ce ne sont
|
|
||||||
pas des `Resource`. À poser maintenant coûte une ligne, plus tard coûte de le redécouvrir.
|
|
||||||
- **Origine de la page de capture dans le CORS** — case à cocher, pas encore une valeur : la config CORS
|
|
||||||
est au niveau du bucket et ne liste que les origines du manager. Tant que l'hébergement de la surface
|
|
||||||
web non-Flutter (éditeur POI + page de capture) n'est pas tranché, on ne sait pas si c'est une origine
|
|
||||||
de plus ou la même. À revérifier au lot 11bis.
|
|
||||||
- **Règles Storage : écriture fermée à tous les clients**, lecture inchangée. Relever les règles
|
- **Règles Storage : écriture fermée à tous les clients**, lecture inchangée. Relever les règles
|
||||||
actuelles avant. ⚠️ À appliquer **après** la mise en prod du nouveau manager-app : fermer avant
|
actuelles avant. ⚠️ À appliquer **après** la mise en prod du nouveau manager-app : fermer avant
|
||||||
casse l'upload de la version encore déployée.
|
casse l'upload de la version encore déployée.
|
||||||
|
|||||||
@ -1,233 +0,0 @@
|
|||||||
# Menu VR — bento, et le reste de l'onglet VR
|
|
||||||
|
|
||||||
> Chantier **V2**, analysé dans le code le **2026-09-16**.
|
|
||||||
> Suite de `vr-quest-unity-plan.md` (lot XR-4, item E5 — le menu flottant existe et tourne).
|
|
||||||
> Ce document ne rouvre pas les décisions du plan VR : il traite **quatre écarts** entre ce
|
|
||||||
> que le manager laisse configurer et ce que le casque affiche réellement.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Le constat
|
|
||||||
|
|
||||||
L'onglet **Main / VR** du manager ([`vr_screen.dart`](../../manager-app/lib/Screens/Vr_devices/vr_screen.dart))
|
|
||||||
réutilise `AppConfigurationLinkScreen`, l'écran de configuration partagé avec le mobile, le web
|
|
||||||
et le kiosk. Il hérite donc de tout son outillage — dont un **sélecteur de forme bento**
|
|
||||||
(les quatre petits boutons carré / large / haut / grand) qui, côté casque, ne pilote rien.
|
|
||||||
|
|
||||||
Vérifié :
|
|
||||||
|
|
||||||
| Fait | Preuve |
|
|
||||||
|---|---|
|
|
||||||
| Les spans sont portés par le **lien de configuration**, pas par les sections | `AppConfigurationLink.cs:35` — commentaire d'origine : *« Specific Mobile & Web »* |
|
|
||||||
| Ils ne pilotent que l'écran « choisis ta configuration » | `mymuseum-visitapp/lib/Screens/Home/home_3.0.dart:1179`, `visitapp-web/src/components/ConfigurationGrid.tsx:38` |
|
|
||||||
| Le casque ne les lit jamais | `ConfigurationExport.cs` n'a aucun champ span ; `grep -r "Span" Assets/Scripts` → rien |
|
|
||||||
| Un casque est appairé à **une seule** configuration | `MenuBootstrap.cs:80` — `pairing.ConfigurationId`. Il n'y a pas d'écran de choix de configuration en VR |
|
|
||||||
| Le menu VR affiche les **sections racines**, toutes à la même taille | `FloatingMenu.cs:106-125` — arc de 5 panneaux par rangée, `0,50 × 0,34 m` fixes |
|
|
||||||
|
|
||||||
Autrement dit : en VR, le bento du manager porte sur un objet (la configuration) qui n'a pas
|
|
||||||
d'écran de sélection, alors que l'objet qui *a* un écran (la section) n'a pas de spans.
|
|
||||||
|
|
||||||
Deux autres écarts, plus petits, de la même famille « écran partagé, canal différent » :
|
|
||||||
la notification et la popup d'édition d'un casque.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Décisions actées
|
|
||||||
|
|
||||||
| Décision | Raison |
|
|
||||||
|---|---|
|
|
||||||
| **Le bento VR porte sur les sections**, pas sur les configurations | C'est le seul écran de choix qui existe dans le casque. Sans ça, les boutons resteront décoratifs quoi qu'on fasse |
|
|
||||||
| **L'arc est conservé** | Ce n'est pas un défaut de rendu : un mur plat oblige à regarder ses bords de biais. La grille bento est calculée en cellules, puis chaque cellule est *mappée* sur un angle + une hauteur |
|
|
||||||
| **Grille cible : 6 colonnes × 2 rangées max** | Le confort visuel en VR c'est ±60° horizontaux ; au-delà on encercle le visiteur. Format naturellement paysage, proche du rendu web |
|
|
||||||
| **Une popup casque distincte de celle du kiosk** | La moitié des champs de la popup partagée est morte en VR (voir lot 4) — les masquer un par un dans un widget commun coûte plus cher que d'en écrire un |
|
|
||||||
| Les spans de section **ne sont pas exposés au mobile/web en V1** | Ils ont déjà leur bento, au niveau configuration. Ouvrir un second niveau de grille sur ces fronts est un chantier produit à part |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Lot 1 — La notification annonce la mauvaise app
|
|
||||||
|
|
||||||
**~15 minutes, indépendant des autres.**
|
|
||||||
|
|
||||||
Modifier un contenu VR affiche « application mobile mise à jour ». Le paramètre prévu pour ça
|
|
||||||
existe déjà et n'est simplement pas renseigné :
|
|
||||||
|
|
||||||
```
|
|
||||||
app_configuration_link_screen.dart:144
|
|
||||||
final appUpdatedMsg = widget.appUpdatedLabel ?? AppLocalizations.of(context)!.appUpdatedSuccess;
|
|
||||||
↑ jamais passé par vr_screen.dart
|
|
||||||
```
|
|
||||||
|
|
||||||
À faire :
|
|
||||||
|
|
||||||
1. Clé `vrAppUpdatedSuccess` dans `app_fr.arb` (template), puis `app_en.arb` et `app_nl.arb`.
|
|
||||||
2. `vr_screen.dart:38` — passer `appUpdatedLabel: l.vrAppUpdatedSuccess` à `AppConfigurationLinkScreen`.
|
|
||||||
3. `vr_screen.dart:78` (`_backgroundCard`) — même remplacement, c'est un `showNotification` séparé.
|
|
||||||
4. `vr_devices_tab.dart:280` (`_edit`) — idem.
|
|
||||||
|
|
||||||
⚠️ Le template i18n est `app_fr.arb` ; les trois langues sont obligatoires (cf. `manager-app/CLAUDE.md`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Lot 2 — Des spans par section (backend + manager)
|
|
||||||
|
|
||||||
**Le socle du lot 3. Rien de visible dans le casque avant le lot 3.**
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
```
|
|
||||||
Data/Section.cs + int? GridColSpan, int? GridRowSpan
|
|
||||||
DTOs/SectionDTO.cs + gridColSpan, gridRowSpan (après `order`, même famille)
|
|
||||||
→ mapping dans ToDTO / FromDTO des champs communs
|
|
||||||
Migrations/ AddSectionGridSpans
|
|
||||||
```
|
|
||||||
|
|
||||||
L'export est **gratuit** côté transport : `/api/configuration/{id}/export`
|
|
||||||
(`ConfigurationController.cs:437`) sérialise déjà les `SectionDTO` complets via
|
|
||||||
`SectionFactory.ToDTO`. Les nouveaux champs suivent.
|
|
||||||
|
|
||||||
⚠️ `Section.ToDTO()` n'est pas virtuelle — vérifier que la copie des champs communs se fait
|
|
||||||
bien au même endroit que `order`, sinon les spans sortiront nuls sur les sous-types.
|
|
||||||
⚠️ `dotnet ef` ignore `appsettings.Development.json` : passer par `MIGRATIONS_CONNECTION`.
|
|
||||||
|
|
||||||
### Client API
|
|
||||||
|
|
||||||
`manager-app/manager_api_new/` — **éditer les fichiers générés à la main**, ne jamais relancer
|
|
||||||
la génération OpenAPI (règle projet).
|
|
||||||
|
|
||||||
### Manager
|
|
||||||
|
|
||||||
L'onglet « Contenu de l'application VR » ne doit plus lister des *configurations* avec des
|
|
||||||
spans, mais les *sections racines* de la configuration, avec leurs spans et un aperçu.
|
|
||||||
|
|
||||||
Deux options, à trancher en début de conversation :
|
|
||||||
|
|
||||||
- **a)** Un écran VR dédié, qui reprend le sélecteur de forme et l'aperçu live de
|
|
||||||
`AppConfigurationLinkScreen` mais s'alimente en sections. Plus propre, plus de code.
|
|
||||||
- **b)** Paramétrer `AppConfigurationLinkScreen` pour qu'il accepte une source « sections ».
|
|
||||||
Moins de code, mais l'écran sert déjà quatre canaux — il est le point de contact de tous
|
|
||||||
ces écarts, l'élargir encore le rend plus difficile à corriger.
|
|
||||||
|
|
||||||
Recommandation : **(a)**, c'est précisément l'accumulation de cas particuliers dans cet écran
|
|
||||||
partagé qui produit les trois autres lots de ce document.
|
|
||||||
|
|
||||||
À réutiliser tel quel : le debounce de 400 ms sur l'enregistrement des spans
|
|
||||||
(`app_configuration_link_screen.dart:89-98`) — un slider de forme qui écrit à chaque frame
|
|
||||||
sature l'API.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Lot 3 — Le rendu bento dans le casque (Unity)
|
|
||||||
|
|
||||||
### Ce qui change
|
|
||||||
|
|
||||||
`FloatingMenu.Place(panel, index, total)` positionne aujourd'hui par index brut :
|
|
||||||
`row = index / 5`, angle = pas fixe. Il faut passer par une grille.
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Placement dense par spans → chaque section obtient (col, row, colSpan, rowSpan)
|
|
||||||
2. Mapping cellule → arc → angle = (col + colSpan/2 - cols/2) * StepDegrees
|
|
||||||
hauteur = HeightMeters - row * (cellH + gap)
|
|
||||||
3. Taille du quad → width = colSpan * cellW + (colSpan-1) * gap
|
|
||||||
height = rowSpan * cellH + (rowSpan-1) * gap
|
|
||||||
```
|
|
||||||
|
|
||||||
L'algorithme de placement dense existe en Dart dans `manager-app/myinfomate_layout/`
|
|
||||||
(le même que le mobile). Deux voies :
|
|
||||||
|
|
||||||
- **Le porter en C#** (~150 lignes) — le casque reste autonome, cohérent avec le cache-d'abord.
|
|
||||||
- **Le faire calculer par le backend** dans l'export — une seule implémentation, mais le
|
|
||||||
casque devient dépendant d'un champ calculé, et le cache d'un vieux contenu porte un vieux
|
|
||||||
placement.
|
|
||||||
|
|
||||||
Recommandation : **le porter en C#**, l'export reste une description, pas une mise en page.
|
|
||||||
|
|
||||||
### Fichiers
|
|
||||||
|
|
||||||
```
|
|
||||||
Assets/Scripts/Net/ConfigurationExport.cs + GridColSpan / GridRowSpan sur SectionSummary
|
|
||||||
Assets/Scripts/Menu/FloatingMenu.cs Place() → grille ; MaxPerRow 5 → 6
|
|
||||||
Assets/Scripts/Menu/MenuItemPanel.cs Create() prend une taille au lieu des const
|
|
||||||
Assets/Scripts/Menu/BentoLayout.cs (neuf) le placement dense
|
|
||||||
```
|
|
||||||
|
|
||||||
⚠️ `MenuItemPanel.WidthMeters` / `HeightMeters` sont des `const` **publiques**, lues ailleurs
|
|
||||||
(`FloatingMenu.Place`, et à vérifier dans `PagedView` / `SectionPages`). Les rendre variables
|
|
||||||
d'instance sans casser ces usages — garder les const comme valeurs de cellule par défaut est
|
|
||||||
le chemin le plus court.
|
|
||||||
|
|
||||||
⚠️ La cascade d'apparition (`PlayAppear(i * 0.04f)`) doit suivre l'ordre de **lecture** de la
|
|
||||||
grille (rangée par rangée, gauche à droite), pas l'index de la liste : avec des spans, les
|
|
||||||
deux divergent.
|
|
||||||
|
|
||||||
⚠️ Un panneau `colSpan = 2` couvre ~32° d'arc. Un quad plat reste acceptable à cette ouverture ;
|
|
||||||
au-delà (un hypothétique span 3) il faudrait le subdiviser. Ne pas le faire avant d'en avoir besoin.
|
|
||||||
|
|
||||||
### Vérification
|
|
||||||
|
|
||||||
Sans casque, le viewer web (`vr-app/viewer/`) est le chemin court pour valider un placement.
|
|
||||||
Avec casque : `adb push` d'un `scene.json`, l'app le préfère à celui embarqué.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Lot 4 — La popup d'édition d'un casque
|
|
||||||
|
|
||||||
**Indépendant. ~une demi-journée.**
|
|
||||||
|
|
||||||
`showChangeInfo` ([`Kiosk_devices/change_device_info_modal.dart:16`](../../manager-app/lib/Screens/Kiosk_devices/change_device_info_modal.dart))
|
|
||||||
est partagée avec le kiosk : largeur figée à 580, une colonne scrollée, et deux boutons
|
|
||||||
`RoundedButton` dont les libellés **« Annuler » / « Changer » sont en dur** (violation i18n).
|
|
||||||
|
|
||||||
Le vrai problème n'est pas la mise en page : **la moitié des champs ne sert à rien en VR.**
|
|
||||||
Aucun script Unity ne lit ces réglages — `grep -r` sur `Assets/Scripts` : zéro occurrence.
|
|
||||||
|
|
||||||
| Champ | En VR |
|
|
||||||
|---|---|
|
|
||||||
| `name`, `configurationId` | **gardés** |
|
|
||||||
| `primaryColor`, `secondaryColor` | morts — le menu a sa propre palette de panneaux |
|
|
||||||
| `loaderImageId` | mort — un casque n'a pas d'écran de chargement 2D |
|
|
||||||
| `screenPercentageSectionsMainPage`, `roundedValue` | morts — ce sont des réglages d'écran plat |
|
|
||||||
| `isSectionImageBackground`, `isHour`, `isDate` | morts |
|
|
||||||
|
|
||||||
À faire : `Vr_devices/change_headset_modal.dart`, paysage deux colonnes (~940 px), avec
|
|
||||||
identité + configuration à gauche, fond immersif à droite (il vit aujourd'hui dans le panneau
|
|
||||||
latéral de l'onglet contenu — à décider : déplacé ou dupliqué). Boutons repris du langage
|
|
||||||
visuel de la Médiathèque / du Studio IA plutôt que des `RoundedButton` historiques.
|
|
||||||
|
|
||||||
Tout le texte passe par `AppLocalizations`, y compris les deux boutons.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ordre
|
|
||||||
|
|
||||||
```
|
|
||||||
Lot 1 ────────────────────────────── indépendant, à faire en premier (15 min)
|
|
||||||
Lot 4 ────────────────────────────── indépendant
|
|
||||||
Lot 2 ───→ Lot 3 le 3 n'affiche rien sans le 2
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mini-prompts de lancement
|
|
||||||
|
|
||||||
Un lot = une conversation. Chaque prompt suppose que ce fichier est lisible.
|
|
||||||
|
|
||||||
**Lot 1**
|
|
||||||
> Lis `DOCS/v2/vr-menu-bento-plan.md` § Lot 1 et applique-le : la notification de l'onglet VR
|
|
||||||
> du manager annonce « application mobile ». Ajoute la clé i18n dans les trois langues et passe
|
|
||||||
> `appUpdatedLabel` aux quatre endroits listés.
|
|
||||||
|
|
||||||
**Lot 2**
|
|
||||||
> Lis `DOCS/v2/vr-menu-bento-plan.md` § Lot 2. Ajoute `gridColSpan` / `gridRowSpan` sur
|
|
||||||
> `Section` côté `manager-service` (entité, DTO, migration, client API édité à la main), puis
|
|
||||||
> fais que l'onglet « Contenu de l'application VR » du manager édite les spans **des sections**
|
|
||||||
> et non des configurations. Commence par me dire si tu pars sur l'option (a) ou (b) du plan.
|
|
||||||
|
|
||||||
**Lot 3**
|
|
||||||
> Lis `DOCS/v2/vr-menu-bento-plan.md` § Lot 3. Dans `vr-app/unity`, fais que le menu flottant
|
|
||||||
> place ses panneaux sur une grille bento par spans mappée sur l'arc, au lieu de l'index brut
|
|
||||||
> actuel. Le lot 2 doit être fait — vérifie que l'export porte bien les spans avant de commencer.
|
|
||||||
|
|
||||||
**Lot 4**
|
|
||||||
> Lis `DOCS/v2/vr-menu-bento-plan.md` § Lot 4. Écris une popup d'édition de casque dédiée en
|
|
||||||
> paysage deux colonnes, sans les champs morts en VR, avec les boutons du langage visuel de la
|
|
||||||
> Médiathèque, et tout le texte en i18n FR/EN/NL.
|
|
||||||
Loading…
x
Reference in New Issue
Block a user