23 KiB
VR Meta Quest — app Unity + onglet XR dans manager-app — 📦 V2
Relevé le 2026-08-31. Toutes les affirmations « existe / n'existe pas » ci-dessous ont été vérifiées dans le code, pas dans la doc. Ce fichier remplace la section VR de ../roadmap.md comme référence technique — la roadmap garde le volet commercial.
⚠️ Rien de ce document n'est V1. La bascule Postgres et le backlog V1 passent avant.
0. Correction d'une affirmation de STATUS.md
STATUS.md §5 disait jusqu'ici : VR Meta Quest = « idée, zéro ligne de code dans tous les repos ». C'est faux depuis au moins juillet 2026. Le canal VR est déjà un citoyen de première classe du modèle de données et de la navigation du back-office. Ce qui manque, c'est l'app et l'écran de flotte — pas les fondations.
1. Ce qui existe déjà
1.1 Backend (manager-service)
| Brique | Emplacement | État |
|---|---|---|
AppType.VR |
Data/ApplicationInstance.cs — enum AppType { Mobile, Tablet, Web, VR, Voice } |
✅ Valeur 3, persistée en base |
Instance.IsVR |
Data/Instance.cs:35, DTOs/InstanceDTO.cs:16, InstanceController.cs:212 |
✅ Activable par instance, exposé au DTO, modifiable par l'API |
| Migration | 20250709151627_AddedApplicationInstanceAndMisc |
✅ Colonne IsVR en base depuis juillet 2025 |
ApplicationInstance VR |
Controllers/ApplicationInstanceController.cs — CRUD complet + application-link |
✅ Une instance VR se crée déjà par l'API, sans une ligne de code neuve |
AppConfigurationLink |
Data/AppConfigurationLink.cs |
✅ Porte déjà DeviceId (nullable) — c'est exactement le mécanisme « une config par appareil » du kiosk |
Entité Device |
Data/Device.cs |
✅ Identifier, Name, Connected, BatteryLevel, ConnectionLevel, AppVersion, LastSeen, InstanceId, ConfigurationId |
DeviceController |
7 endpoints (list, detail, create, update, mainInfos, delete) | ⚠️ Existe, mais hardcodé sur AppType.Tablet (DeviceController.cs:155) |
| Push MQTT « config changée » | DeviceController.cs:303 → topic player/{device.Id} |
✅ Le mécanisme de rafraîchissement à distance est déjà là |
| Télémétrie | Data/VisitEvent.cs:27 — public AppType AppType |
✅ Un event VR se stocke sans aucun changement de schéma |
| Export de contenu | GET /api/configuration/{id}/export (ConfigurationController.cs:388) |
✅ Configuration + sections + ressources en un seul appel — le contrat idéal pour un renderer tiers |
| Auth app cliente | GET /api/instance/appKeyByPin (InstanceController.cs:347) |
⚠️ Existe, mais ApiKeyAppType ne connaît que { VisitApp, TabletApp, Other } |
1.2 manager-app
| Brique | Emplacement | État |
|---|---|---|
AppType.VR côté client |
manager_api_new/lib/model/app_type.dart + app_type_extensions.dart |
✅ Valeur 3 présente et nommée « VR » |
| Sous-menu « VR » | lib/Screens/Main/main_screen.dart:65 — if (instance.isVR == true) |
✅ L'entrée de menu apparaît déjà si l'instance a le canal activé |
| Écran VR | main_screen.dart:698-705 — case 'vr' |
❌ Text("TODO vr") — c'est littéralement le seul trou de la navigation |
| Écran de flotte à cloner | lib/Screens/Kiosk_devices/ — 4 fichiers, 769 lignes |
✅ kiosk_screen.dart (grille d'appareils), device_element.dart, dropDown_configuration.dart (assignation de config), change_device_info_modal.dart |
| Filtre stats par canal | lib/Screens/Statistics/statistics_screen.dart:81, 177 |
✅ Générique sur AppType.values, et statsChannelVR existe déjà en i18n — le canal VR apparaîtra tout seul dès le premier VisitEvent |
1.3 Ce qui n'existe nulle part
- Aucun projet Unity, aucun
.unity, aucun package Meta XR dans les repos. - Aucun rendu 360° : pas de
ResourceType.Panorama360(planifié en V2, voir ../todo-features.md § Ressource 360°), pas de flag « vidéo equirectangulaire ». - Aucun
Devicenon-tablette : la table est peuplée exclusivement partablet-app.
2. Ce qu'il reste à faire
Lot V-1 — Le Device devient multi-canal (backend) — 2-3 j
Aujourd'hui un appareil est une tablette : DeviceController.Create va chercher
ApplicationInstances.FirstOrDefault(ai => ai.AppType == AppType.Tablet) en dur. Un casque
enregistré par ce chemin serait rattaché à l'instance kiosk et apparaîtrait dans l'onglet Kiosk.
Device.AppType(colonneint, défautTablet= 1 pour que les lignes existantes ne bougent pas) + migration EFDeviceDTO.appType/DeviceDetailDTO, mappé dansToDTO/ToDetailDTO/FromDTODeviceController.Create: résoudre l'ApplicationInstancesurnewDevice.appTypeau lieu deAppType.TabletDeviceController.GetAll: paramètre de requêteappTypeoptionnel (sans lui, comportement actuel)ApiKeyAppType: ajouterVrAppà la fin de l'enum ({ VisitApp, TabletApp, Other, VrApp }) — l'enum est persisté en int, ne jamais insérer au milieuDeviceControllerTests: un cas « création d'un device VR » qui vérifie le rattachement à la bonneApplicationInstance
⚠️ Le champ Device.ConfigurationId est marqué [Required] et le commentaire du code le dit
« OLD WAY → AppConfigurationLink ». Le casque doit passer par AppConfigurationLink, pas par ce
champ ; on le remplit quand même pour ne pas casser la contrainte.
Lot V-2 — Onglet XR dans manager-app — 4-5 j
C'est le lot le moins cher : l'écran kiosk est un patron directement réutilisable, la seule
différence de fond est l'AppType filtré.
lib/Screens/Vr_devices/vr_screen.dart— clone dekiosk_screen.dart: grille de casques, pincode d'appairage en tête, état connecté/déconnecté- Réutiliser tel quel
device_element.dart,dropDown_configuration.dart(assignation d'une configuration par casque),change_device_info_modal.dart - Brancher
main_screen.dart:704: remplacerText("TODO vr")parVrScreen(applicationInstanceDTO: applicationInstanceVR) - Carte casque : ajouter niveau de batterie +
AppVersion+LastSeen(colonnes déjà en base, jamais affichées pour le kiosk) manager_api_new: ajouterappTypeàdevice_dto.dart/device_detail_dto.dartetVrAppàapi_key_app_type.dart— à la main, la génération OpenAPI ne se relance pas sur ce repo- i18n FR/EN/NL/DE : titre d'onglet, « casques », « aucun casque appairé », libellés batterie/version
Rien à faire pour le sous-menu ni pour les stats : les deux sont déjà branchés sur isVR / AppType.VR.
Lot V-3 — Contrat de contenu pour un client non-Flutter — 1-2 j
Unity ne peut pas consommer le client Dart généré. Deux options :
| Option | Coût | Verdict |
|---|---|---|
| Unity appelle les endpoints REST un par un, DTO C# écrits à la main | Élevé et récurrent — chaque évolution de section se répercute | ❌ |
Unity appelle GET /api/configuration/{id}/export (un appel, tout le contenu) et désérialise ExportConfigurationDTO |
Faible — le DTO d'export est déjà stable, c'est celui de l'import/export back-office | ✅ retenu |
- Vérifier que
Exportrépond bien à une clé d'API d'app (aujourd'hui utilisé depuis le back-office authentifié) - Documenter le JSON d'export comme contrat public versionné (c'est aussi ce qui servira au mode offline du casque)
- Décider si l'export multilingue (aujourd'hui
?language=) doit renvoyer toutes les langues d'un coup pour la VR — un casque en borne change de langue à chaud
Lot V-4 — L'app Unity + Meta XR SDK — 6-10 semaines (POC utile : 3-4 semaines)
- Socle : Unity 6 LTS, URP, XR Plugin Management → OpenXR + Meta XR feature group, Meta XR Core SDK + Interaction SDK (raycast contrôleur et hand tracking)
- Couche réseau C# : appairage par pincode (clone du flux
tablet-app/lib/Screens/Configuration/config_view.dart:274), récupération de la clé d'API,POST /api/deviceavecappType = VRet l'identifiant matériel du casque - Cache disque + offline-first : une borne d'accueil perd le wifi ; l'export JSON et les médias se mettent en cache, l'app démarre sur le cache et se rafraîchit en tâche de fond
- Renderer par type de section — arbitrage tranché, voir §8
- Lecture 360° : dépend du lot « Ressource 360° » de todo-features.md — sans un type de ressource 360°, il n'y a rien à afficher en immersif
- Mode borne : verrouiller le casque sur l'app (Meta Horizon Managed Solutions ou un MDM tiers), relance auto au réveil, timeout de session, retour au menu à la repose du casque
- Télémétrie :
POSTdeVisitEventavecAppType.VR→ les stats manager-app fonctionnent sans une ligne de plus - Distribution : Meta Horizon Store (review) ou canal privé / sideload via MDM
Lot V-5 — Supervision de la flotte — 1-2 semaines
- Client MQTT dans Unity (MQTTnet) : heartbeat, batterie, abonnement à
player/{deviceId}— le publish côté serveur existe déjà (DeviceController.cs:303) - Alternative pour le POC : polling de l'export toutes les N minutes. Moins élégant, 10× moins cher, suffisant pour 1-3 casques
- Remontée batterie/version/dernier vu (colonnes déjà présentes sur
Device)
3. Estimation
| Lot | Charge | Dépendances |
|---|---|---|
| V-1 — Device multi-canal (backend) | 2-3 j | — |
| V-2 — Onglet XR manager-app | 4-5 j | V-1 |
| V-3 — Contrat de contenu | 1-2 j | — |
| Sous-total « back-office XR prêt » | 8-12 j | |
| V-4 — App Unity (POC : menu + vidéo 360 + une section) | 3-4 sem. | V-1, V-3, Ressource 360° |
| V-4 — App Unity (couverture complète) | 6-10 sem. | idem |
| V-5 — Supervision flotte | 1-2 sem. | V-4 |
| Total avec app complète | ~3-4 mois d'un dev |
Le découpage est volontaire : les 8-12 jours du back-office XR se tiennent debout tout seuls. Ils rendent le canal VR administrable et démontrable (flotte, assignation de config, stats) avant d'engager le vrai coût, qui est l'app Unity.
4. Risques
- Le contenu du CMS est 2D. C'est le risque produit majeur, pas un risque technique. Un Article affiché sur un panneau flottant en VR est moins bon qu'une tablette. La valeur du canal vient du contenu 360°, que très peu de clients possèdent. Sans un client pilote qui a déjà des vidéos 360°, le POC démontrera une régression.
- Nouvelle stack, zéro mutualisation. Unity/C# XR ne réutilise ni Flutter, ni le client API
généré, ni
myinfomate_layout. C'est un quatrième front à maintenir, pas une déclinaison. Économie du MDM.Rétrogradé le 2026-08-31. Un MDM XR (~15-20 €/casque/mois) n'est nécessaire qu'à l'échelle d'une flotte. À 1-3 casques, sideload adb + le mode appareil intégré suffisent. Ne redevient un sujet qu'au-delà d'une dizaine de casques. ⚠️ Les noms et l'état des programmes business de Meta bougent vite — à revérifier avant de l'écrire dans une offre.- Exploitation en borne publique : hygiène des casques, sessions courtes, présence de personnel. C'est un frein opérationnel réel côté client, indépendant de la qualité de l'app.
- La landing promet déjà « Meta Quest — Bientôt » en 4 langues
(
myinfomate-landing,translations.ts, clémode4Desc). L'écart annonce/réalité existe depuis un moment ; il se creuse tant que rien n'est livré.
5. Recommandation de séquencement
- V-1 + V-2 + V-3 (~2 semaines) — peu cher, sans risque, rend le canal administrable, ferme le
TODO vr. - Ressource 360° (déjà en V2, le plus petit des trois chantiers médias) — prérequis dur du POC.
- Décision go/no-go conditionnée à un client pilote avec du contenu 360° existant. Sans lui, ne pas démarrer V-4.
- POC Unity (3-4 semaines) — menu immersif + vidéo 360 + une section riche.
- V-4 complet et V-5 seulement après validation du POC en conditions réelles.
§6 à §10 ajoutés le 2026-08-31, seconde passe. Tout ce qui suit est vérifié dans le code — c'est de l'analyse déjà faite, à ne pas refaire.
6. Pricing — add-on « Contenu immersif », à partir de 70 €/mois
Arbitré par Thomas le 2026-08-31. Les chiffres encore inscrits dans
../roadmap.md — +30-50€/mois (L113) et +40€/mois dans le bundle (L148-149) —
sont périmés et à réécrire.
Historique de la décision : l'assistant avait proposé un palier « Immersif » à 299 €/mois en remplacement de l'add-on. Arbitrage retenu : on garde un add-on, revalorisé de 40 à 70 €/mois et plus. Le raisonnement du palier est conservé ci-dessous parce que son principe structurant, lui, est validé — c'est le montant et le véhicule qui changent.
Le principe qui reste vrai : décorréler du casque
Un add-on « VR » ne se vend que le jour où l'app Unity existe et où le client achète un casque. Un add-on « Contenu immersif » vend le contenu — image 360, vidéo 360, modèles GLB — qui s'affiche déjà dans le web, le mobile et le kiosk. Le casque devient le haut de gamme de l'add-on, pas sa condition d'existence. Un client souscrit et en a pour son argent sans casque.
Pourquoi l'add-on bat le palier — le point que l'assistant avait manqué
Un add-on s'accroche à n'importe quel palier. Le palier à 299 € forçait tout le monde à passer par Premium ; l'add-on permet à un client Pro à 99 € de prendre du 360 sans payer l'assistant IA dont il ne veut pas → 169 € au lieu de 299 €. Marché adressable nettement plus large.
Sur le total, les deux modèles convergeaient de toute façon : Premium 179 + 70 = 249 €, soit exactement le plancher de négociation cité pour le palier. Le désaccord portait sur la structure, pas sur le montant.
Paliers de référence (source de vérité : myinfomate-landing, HomeClient.tsx:534,583 +
segments.ts) : Essentiel 39 / Pro 99 / Premium 179 / Enterprise sur devis.
Ce que l'add-on débloque
Ressources image 360, vidéo 360, modèles GLB avec POI cliquables texte/audio — et l'app Quest quand elle sortira. L'assistant IA n'est pas dedans : il reste attaché à son propre add-on / au palier Premium.
⚠️ Le stockage — plus aigu en add-on qu'en palier
C'est le seul point de vigilance qui s'aggrave avec ce modèle. Un client Pro à 99 € a un quota dimensionné pour du Pro, et une vidéo 360 de 5 min en qualité correcte pèse des Go.
➡️ L'add-on doit porter sa propre enveloppe de stockage, explicite et finie, avec facturation du dépassement. Sans ça, le premier client 360 sur un petit palier fait sauter la marge.
Ce qui reste HORS de l'add-on
| Exclusion | Pourquoi |
|---|---|
| Modélisation 3D | Coût studio, 3 à 15 k€, variable — reste sur devis |
| Hardware casque | Achat client |
| Stockage au-delà de l'enveloppe | Voir ci-dessus — plafond dur + facturation du dépassement |
Onboarding one-shot : 1200-1500 € quand il y a des casques (provisioning, config borne, formation, journée sur site). Sans casque, l'add-on ne justifie pas de frais de mise en place particuliers.
Implémentation — une journée de backend
Vérifié dans le code, et c'est la bonne surprise. L'Instance recopie localement les
valeurs du plan au lieu de les lire à travers : StorageQuotaBytes, AiTokensPerMonth,
HasStats, StatsHistoryDays, HasAdvancedStats sont tous des colonnes de Instance
(Data/Instance.cs:87-98). Le SubscriptionPlan n'est qu'un gabarit.
Et IsAssistant (Data/Instance.cs:37) est déjà exactement ce patron : un add-on = un bool
sur l'Instance.
Instance.HasImmersiveContent(bool) + migration- Relèvement de
StorageQuotaBytesà l'activation - Exposition dans
InstanceDTO+ l'écran SuperAdmin
➡️ Pas de nouvelle ligne de plan, pas de table de jointure, pas de refonte de la facturation.
Et ça tombe sur une carte déjà planifiée : « Écran SuperAdmin — add-on IA & canaux d'une
instance » (kanban, colonne Planifié), qui dit explicitement « à faire d'un bloc avec l'endpoint
de mise à jour d'ApplicationInstance : c'est le même écran ». L'add-on immersif y prend sa place
sans rien ajouter au backlog.
⚠️ Réserve Stripe : le webhook ne gère que checkout.session.completed et
invoice.payment_failed (Controllers/StripeWebhookController.cs:62-65), rien sur les lignes
d'abonnement. Facturer l'add-on automatiquement demandera du travail. Pour 1-3 clients
pilotes, une ligne ajoutée à la main dans Stripe suffit.
7. Assistant IA dans le casque — possible, et quasi gratuit
Vérifié dans le code. POST /api/Ai/chat (Controllers/AiController.cs:367) prend déjà un
AppType dans sa requête (DTOs/AiChatDTO.cs:10) et se contente de vérifier que
l'ApplicationInstance de cet AppType a IsAssistant = true (AiController.cs:387).
➡️ Créer l'ApplicationInstance VR avec le flag suffit. Zéro ligne de backend.
Deux nuances :
- Pas de fallback pour VR. Le fallback sur Mobile n'existe que pour
AppType.Voice(commentaireAiController.cs:387). SansApplicationInstanceVR, c'est un 403. - Le TTS est côté client. L'instance ne stocke qu'un nom de voix Gemini (
Data/Instance.cs:54), la synthèse est faite par l'app. Unity devra appeler Gemini TTS lui-même et gérer le micro pour le STT. L'architecture est prouvée par le POC Ray-Ban (11 implémentations de moteurs, brancheMeta-Rayban-Test) mais c'est du Dart : réimplémentation en C#, pas un portage.
8. Types de section en VR — arbitrage tranché
Sur les 13 types existants :
| Verdict | Types | Pourquoi |
|---|---|---|
| Nécessaire | Menu | C'est le hub flottant lui-même |
| Gagne vraiment | Video (360 en skybox), Slider (galerie immersive), Map (le plan posé devant soi comme une maquette — c'est là qu'arrive le GLB), Parcours (chaque étape = un lieu autour de soi) | La VR ajoute une dimension que l'écran n'a pas |
| Utile en borne | Event / Agenda | « Ce qui se passe aujourd'hui » en écran d'attente d'office de tourisme — c'est même l'usage principal |
| Correct | Quiz | Jouable au regard ou au contrôleur, bon pour l'engagement |
| À ne pas porter | Article, PDF, Web, Weather, Game | Un mur de texte flottant est moins bon qu'une tablette. Une WebView en VR est frustrante |
Scope du POC : Menu + Video 360 + Slider + Map/GLB, avec Event en écran d'attente. Quatre types, et c'est exactement la démo.
9. POI sur modèle GLB — le modèle de données existe déjà à 80 %
La plus grosse économie du chantier, repérée le 2026-08-31.
GeoPoint (Data/SubSection/SectionMap.cs:85) porte déjà Title, Description, ImageResource,
et surtout Contents — une List<Content> où chaque Content (Data/SubSection/Content.cs)
est titre + description + Resource, donc audio. Le tout multilingue via TranslationDTO.
➡️ Un POI sur GLB, c'est le même objet avec une position locale (x,y,z) au lieu d'une lat/lon.
GeoPoint.Model3DPositionnullable (x,y,z + rotation) à côté duGeometryPostGIS existantSectionModel3Dréférençant une ressource GLBResourceType.Model3D— en fin d'enum, il est persisté en int- Éditeur de placement de POI en 3D dans manager-app — le seul morceau non trivial
Le back-office réutilise l'éditeur de POI existant ; le multilingue et l'audio suivent gratuitement.
10. Unity ou WebXR — la question tranchée
Unity n'est PAS une nécessité technique
La distinction qui compte : le mode kiosk ne vient pas du moteur, il vient de la gestion d'appareil. Un MDM ou le mode appareil de Meta verrouille le casque sur n'importe quelle app installée — build Unity, APK Godot, ou navigateur épinglé sur une URL. Le choix du moteur est orthogonal au verrouillage kiosk.
Routes possibles vers un APK installable sur Quest : Unity + Meta XR SDK, Unreal, Godot 4 + OpenXR, ou du WebXR empaqueté (Meta supporte l'installation de PWA sur Horizon OS). ⚠️ Sur ce dernier point : hors date de connaissance de l'assistant, à revérifier avant tout engagement.
Ce qui plaide pour Unity — l'exploitation, pas la technique
- Média offline : plusieurs Go de vidéo 360 en cache navigateur se heurtent aux quotas de stockage. En natif on écrit sur le disque.
- Décodage vidéo : une 360 en 8K veut le décodeur matériel et le contrôle dessus.
- Prévisibilité — l'argument le plus fort : une borne tourne 8 h/jour sans personne. Un build Unity est figé ; le navigateur du casque se met à jour tout seul et peut casser la borne un mardi matin.
- Accessoirement : MQTT en tâche de fond, hand tracking riche, porte du Horizon Store.
Ce qui plaide pour WebXR — à ne pas balayer
visitapp-web rend déjà les 13 types de section en React 19 / Next 16
(src/components/sections/, 13 fichiers, aucune dépendance 3D pour l'instant). Une couche WebXR
par-dessus attaque frontalement le risque n°2 (« nouvelle stack, zéro mutualisation ») :
c'est potentiellement un quatrième front en moins à maintenir.
Position retenue
WebXR pour la maquette et la spec visuelle, Unity pour la borne en production. Pas parce que le web ne peut pas, mais parce qu'une borne publique non surveillée pardonne mal. ➡️ À rouvrir si l'usage devient « le visiteur met le casque 5 minutes avec un agent à côté » — là, le web redevient le bon choix.
Ce que l'assistant peut et ne peut pas produire
Peut : le projet Unity complet — scripts C#, client API, script d'éditeur qui construit la scène
par code (plutôt que du YAML .unity écrit à la main, illisible et cassant), manifest.json avec
les packages Meta XR, menu flottant, swap de skybox 360, chargement GLB, raycast sur POI avec audio.
Ne peut pas : ⚠️ Unity n'est pas installé sur cette machine — vérifié le 2026-08-31, ni
Program Files/Unity, ni Unity Hub. Donc pas de compilation, pas d'APK, pas de rendu, pas de
casque de test. Le livrable est un projet plausible : il faut l'ouvrir et itérer sur les erreurs
de compilation. Compter 2 à 4 allers-retours, surtout sur le SDK Meta dont les API bougent
entre versions.
Pour voir le rendu, la maquette WebXR/three.js est le chemin court : visible sur écran ou dans le
navigateur du Quest en immersif, itérable en secondes. Précédent dans le repo : la démo villa
d'Echternach (visitapp-web/src/app/demo/villa-echternach/scene.ts), rendu procédural fait
exactement pour montrer au client.