Thomas Fransolet 0310d28b5e Canal VR Meta Quest : projet Unity, viewer web et documentation
Premier commit du cinquième front. Trois parties :

- `unity/MyInfoMateVR/` : le projet Unity (6000.0.83f1, URP, Meta XR SDK 205),
  un APK unique pour tous les clients. Menu flottant à sélection au regard,
  appairage, chargement de scène GLB, POI, cache de contenu, télémétrie.
- `unity-overlay/` : les mêmes scripts à recopier sur un projet Unity neuf, avec
  les pièges rencontrés consignés dans son README.
- `viewer/` : viewer et éditeur de scène web autonome (Vite, TypeScript,
  three.js), partagé avec les autres fronts.
- `docs/` : état des lieux, setup Unity, décisions d'architecture et plan
  d'exécution en 9 étapes.

La scène est décrite par un `scene.json` poussé par `adb push` : l'app le
préfère à celui embarqué dans l'APK.

Les binaires (GLB, textures de l'échantillon Sponza, DLL Meta XR) passent par
Git LFS dès ce premier commit — les y faire entrer après coup demanderait de
réécrire l'historique. Les artefacts régénérés par l'éditeur et par CMake
(`Library/`, `.utmp/`, Burst debug) sont ignorés.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 15:26:07 +02:00

89 lines
3.7 KiB
C#

using System;
using UnityEngine;
namespace MyInfoMate.Vr.Net
{
/// <summary>
/// Télémétrie de visite — item <b>E9</b> du lot XR-4.
///
/// <b>Les stats du manager fonctionnent sans une ligne de plus côté serveur</b> :
/// <c>VisitEvent</c> porte déjà un <c>AppType</c>, et l'écran Statistiques filtre
/// génériquement sur <c>AppType.values</c> — le canal VR y apparaîtra tout seul au
/// premier événement reçu.
///
/// ⚠️ Deux différences avec le reste de l'API, vérifiées dans <c>StatsController</c> :
/// la route est <c>POST /api/stats/event</c> (pas <c>/api/visitevent</c>), et
/// <c>appType</c> comme <c>eventType</c> partent en <b>chaîne</b>, parsés par nom
/// côté serveur. Un nom inconnu d'<c>eventType</c> est un 400 ; un <c>appType</c>
/// inconnu retombe <b>silencieusement sur Mobile</b> — c'est-à-dire qu'une faute de
/// frappe ici ferait compter les visites du casque dans le canal mobile.
///
/// L'envoi est « tire et oublie » : une borne ne doit jamais attendre une statistique,
/// et un événement perdu ne vaut pas un message d'erreur au visiteur.
/// </summary>
public class Telemetry
{
/// <summary>Le nom, pas la valeur : le serveur parse par nom (<c>Enum.TryParse</c>).</summary>
const string AppTypeVr = "VR";
readonly ApiClient _client;
readonly string _instanceId;
readonly string _configurationId;
readonly string _language;
/// <summary>
/// Une session = une visite. Sur une borne, elle se renouvelle à chaque retour au
/// menu ou à la repose du casque (E10), pas au lancement de l'app : sinon une
/// journée entière de borne compterait pour un seul visiteur.
/// </summary>
public string SessionId { get; private set; } = NewSessionId();
public Telemetry(ApiClient client, string instanceId, string configurationId, string language)
{
_client = client;
_instanceId = instanceId;
_configurationId = configurationId;
_language = language;
}
public void StartNewSession() => SessionId = NewSessionId();
public void SectionView(string sectionId) => Send("SectionView", sectionId);
public void SectionLeave(string sectionId, int seconds) =>
Send("SectionLeave", sectionId, seconds);
public void MenuItemTap(string sectionId) => Send("MenuItemTap", sectionId);
public void MapPoiTap(string sectionId, string poiTitle) =>
Send("MapPoiTap", sectionId, metadata: $"{{\"poi\":\"{Escape(poiTitle)}\"}}");
async void Send(string eventType, string sectionId,
int? durationSeconds = null, string metadata = null)
{
var result = await _client.PostAsync<object>("/api/stats/event", new
{
instanceId = _instanceId,
configurationId = _configurationId,
sectionId,
sessionId = SessionId,
eventType,
appType = AppTypeVr,
language = _language,
durationSeconds,
metadata,
timestamp = DateTime.UtcNow
});
// La réponse est un 204 sans corps : la désérialisation ne dit rien d'utile,
// seul un échec réseau mérite une trace — et rien ne remonte au visiteur.
if (!result.Ok) Debug.Log($"[Stats] {eventType} non envoyé : {result.Error}");
}
static string NewSessionId() => Guid.NewGuid().ToString("N");
static string Escape(string value) =>
string.IsNullOrEmpty(value) ? "" : value.Replace("\\", "\\\\").Replace("\"", "\\\"");
}
}