# 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](../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](../todo-features.md) § Ressource 360°), pas de flag « vidéo equirectangulaire ». - Aucun `Device` non-tablette : la table est peuplée exclusivement par `tablet-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` (colonne `int`, **défaut `Tablet` = 1** pour que les lignes existantes ne bougent pas) + migration EF - [ ] `DeviceDTO.appType` / `DeviceDetailDTO`, mappé dans `ToDTO` / `ToDetailDTO` / `FromDTO` - [ ] `DeviceController.Create` : résoudre l'`ApplicationInstance` sur `newDevice.appType` au lieu de `AppType.Tablet` - [ ] `DeviceController.GetAll` : paramètre de requête `appType` optionnel (sans lui, comportement actuel) - [ ] `ApiKeyAppType` : ajouter `VrApp` **à la fin** de l'enum (`{ VisitApp, TabletApp, Other, VrApp }`) — l'enum est persisté en int, ne jamais insérer au milieu - [ ] `DeviceControllerTests` : un cas « création d'un device VR » qui vérifie le rattachement à la bonne `ApplicationInstance` ⚠️ 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 de `kiosk_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` : remplacer `Text("TODO vr")` par `VrScreen(applicationInstanceDTO: applicationInstanceVR)` - [ ] Carte casque : ajouter niveau de batterie + `AppVersion` + `LastSeen` (colonnes déjà en base, jamais affichées pour le kiosk) - [ ] `manager_api_new` : ajouter `appType` à `device_dto.dart` / `device_detail_dto.dart` et `VrApp` à `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 `Export` ré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/device` avec `appType = VR` et 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** : `POST` de `VisitEvent` avec `AppType.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 1. **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. 2. **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. 3. ~~**É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. 4. **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. 5. **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 1. **V-1 + V-2 + V-3** (~2 semaines) — peu cher, sans risque, rend le canal administrable, ferme le `TODO vr`. 2. **Ressource 360°** (déjà en V2, le plus petit des trois chantiers médias) — prérequis dur du POC. 3. **Décision go/no-go** conditionnée à **un client pilote avec du contenu 360° existant**. Sans lui, ne pas démarrer V-4. 4. **POC Unity** (3-4 semaines) — menu immersif + vidéo 360 + une section riche. 5. 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](../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` (commentaire `AiController.cs:387`). Sans `ApplicationInstance` VR, 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, branche `Meta-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` 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.Model3DPosition` nullable (x,y,z + rotation) à côté du `Geometry` PostGIS existant - [ ] `SectionModel3D` référençant une ressource GLB - [ ] `ResourceType.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.