Securite
- RequireAppKey : filtre qui exige une cle d'API valide ou un utilisateur du
manager. Pose sur les routes de contenu consommees par les apps visiteur
(Configuration, Section, Resource, SectionMap, SectionEvent, SectionAgenda,
SectionParcours, SectionQuiz, ApplicationInstance). Un [Authorize] d'action
ne peut pas assouplir celui de la classe ; seul [AllowAnonymous] le
court-circuite, et le filtre redevient le controle d'acces. Il ferme
l'enumeration par identifiant, pas la confidentialite : la cle s'obtient
par le slug ou le pincode.
- Instance/slug/{slug} rendait le pinCode, l'adresse de facturation, la TVA et
les quotas a qui lit l'URL du site visiteur. StripCommercialFields est
desormais applique sur slug et byPin ; isTrialActive reste expose pour le
filigrane d'essai.
- Device.Create et Device/{id}/detail repondaient 403 a toute tablette depuis
a452f4a (13/03) : la classe exige InstanceAdmin, une cle ne porte que
AppRead. Ouverts a la cle, le cloisonnement par instance etait deja ecrit.
Canal VR
- Device.AppType (defaut Tablet, backfill a 1) ; Create resout
l'ApplicationInstance sur ce type au lieu de Tablet en dur.
- Get filtre optionnellement par appType ; DeviceDetailDTO expose appVersion
et lastSeen.
- PUT Device/{id}/heartbeat : batterie, version, connexion. Volontairement
etroit, une app ne peut ni se renommer ni changer d'instance.
- ApiKeyAppType.VrApp en fin d'enum.
Contenu immersif
- ResourceType : Image360 (11), Video360 (12), Model3D (13), en fin d'enum.
- Section Scene3D : un modele GLB et ses points d'interet, en objet manipule
ou en decor habite. Les points sont des GeoPoint, avec une LocalTransform
en jsonb (convention glTF) ; CRUD dans SectionScene3DController.
- Instance.HasImmersiveContent : l'add-on ajoute 100 Go au quota de stockage,
repose apres un changement de plan et retire a la desactivation.
- ImmersiveBackground (owned) sur Configuration et ApplicationInstance, avec
une image de repli pour les canaux qui ne rendent pas l'immersif.
Export de configuration
- exportVersion (1) et generatedAt : le JSON devient un contrat, lu tel quel
par l'app Unity.
- Section.ToDTO() n'etant pas virtuelle, l'export ne portait aucun champ
specifique de sous-type. Passe par SectionFactory.ToDTO, et charge les
points des Map et des Scene3D.
- Le fond immersif et son repli partent avec les ressources, URL resolues.
Migrations : AddAppTypeToDevice, AddScene3DSectionAndImmersiveAddon,
AddImmersiveBackground.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ApplicationInstance porte désormais AppName (traductions, jsonb),
IsQRCodeEnabled (défaut true) et les liens App Store / Play Store.
Une ligne existe par (instance, type d'app) : chaque réglage vaut
donc séparément pour le mobile et pour le web.
Les liens stores ne sont modifiables que par un SuperAdmin — c'est
Unov qui publie les apps —, contrôlé côté API et pas seulement masqué
dans l'UI.
La liste anonyme des apps expose le slug web et le nom de l'instance :
la page /download de visitapp-web n'a que l'instanceId d'un QR imprimé.
Réserve les slugs download, demo et api : un segment statique de
visitapp-web l'emporte sur [slug], l'instance serait inaccessible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Updateinstance recopiait les champs un par un et n'avait jamais été étendu
aux champs du guide : GuideName, GuidePersonaPrompt, GuideVoiceId,
IsVisitorQuestionCollectionEnabled et GuideFallbackMessages étaient reçus
puis ignorés, donc l'écran Guide IA ne sauvegardait rien. Reprend les mêmes
gardes que Instance.FromDTO.
Rien n'empêchait deux ApplicationInstance du même AppType sur une instance,
alors que manager-app et AiController résolvent tous deux le canal par un
FirstOrDefault sur (InstanceId, AppType) : un doublon rendait le canal
ambigu, l'assistant pouvant s'afficher activé pendant que l'API répondait
Forbid. Ajoute l'index unique et le contrôle en amont dans Create (409).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Table QuestionThemeMonthly (instance, mois, thème, compteur). C'est elle qui rend
tenable le §8.4 des CGU : le regroupement vivait dans ThemeId, colonne de la ligne
VisitorQuestion, donc la purge du 90e jour l'emportait avec la question et le
client perdait tout au 91e. Elle ne porte que des compteurs — aucune donnée
personnelle, ce qui est précisément ce qui l'autorise à survivre. Insights lit
désormais les thèmes dans cette table, pas dans les questions de la fenêtre.
Liste fixe de 8 thèmes, pas de thèmes découverts par l'IA : des libellés
régénérés à chaque passage donneraient « Horaires » en janvier et « Questions
d'horaires » en février, deux lignes distinctes et une courbe qui ne veut rien
dire — alors que la table existe pour porter cet historique.
Le job tourne à 2 h, la purge à 3 h 30 : une question purgée avant d'avoir été
classée ne compte dans aucun agrégat et rien ne peut la rattraper. Le plafond de
500 par passage ne perd rien, il retarde — les plus anciennes d'abord, un passage
par jour, avertissement si le retard dépasse un passage. Les jetons ne sont pas
décomptés du quota client : il n'a pas demandé ces appels. Un lot en échec n'est
pas marqué « Autre » pour s'en débarrasser, ce serait une perte définitive
maquillée en résultat ; et la relecture se fait par numéro, jamais par position,
pour qu'une ligne manquante ne décale pas les suivantes.
Instance.IsVisitorQuestionCollectionEnabled (défaut true) + garde dans Chat : le
client est responsable de traitement, la collecte était inconditionnelle.
Ajout d'une fabrique design-time : EF construisait tout l'hôte pour trouver le
contexte, et l'hôte ouvre une connexion au démarrage — générer une migration
exigeait donc une base joignable, impossible sur une machine sans Postgres ni
Docker.
dotnet test : 211 passés, 15 sautés, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le client ne pouvait pas savoir qu'une ressource avait change : il ne
recevait que dateCreation. La colonne existait deja sur l'entite, elle
n'etait simplement pas serialisee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Regression introduite par le commit qui journalise les sections.
WeatherSyncService ecrit `section.WeatherResult` sur cron, a 6 h et a 13 h.
Le job existait et etait benin tant que les sections n'etaient pas auditees ;
depuis, chaque rafraichissement produisait une ligne portant la prevision
OpenWeather complete en avant ET en apres -- quelques dizaines de Ko, deux
fois par jour, par section meteo, indefiniment.
Le stockage est le moindre probleme : ce bruit noie les modifications
humaines que l'ecran d'audit existe pour montrer.
Correctif : une liste de colonnes machine (WeatherResult,
WeatherUpdatedDate, DateUpdate) exclues du journal, et surtout aucune ligne
produite quand une modification ne touche qu'elles -- plutot qu'une ligne au
diff vide, qui aurait deplace le bruit sans le retirer.
DateUpdate y est pour une raison distincte de la meteo : estampille a chaque
SaveChanges, il figurait dans tous les diffs sans jamais rien y apprendre. Le
signal etait deja la -- un test ecrit hier devait l'ecarter a la main pour
rester lisible (`newValues.Keys.Where(k => k != "DateUpdate")`). Quand un
test doit filtrer une donnee pour etre lisible, la donnee n'a rien a y faire.
Verifie au passage : AgendaSyncService ecrit des EventAgenda, qui ne sont pas
audites, et ne touche pas la ligne Section. Lui n'est pas concerne.
⚠️ Consequence sur la dette signalee hier : sans le flot meteo, la table ne
grossit plus qu'au rythme des editions humaines -- de l'ordre de quelques
dizaines de milliers de lignes par an sur 4 clients. L'index sur Timestamp
evoque hier n'a plus lieu d'etre, donc pas de migration et le gel du lot B
n'est pas remis en cause. La purge reste pertinente, mais comme sujet RGPD
(lot J), plus comme sujet de volume.
3 tests. dotnet test 203/203.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AuditedTypes.Contains(entry.Entity.GetType()) exigeait l'egalite exacte de
type, or Section est abstraite : le type runtime est toujours SectionMap,
SectionQuiz... Aucune des 13 sortes de section n'etait journalisee -- soit
precisement ce que l'ecran d'audit livre le 12/08 devait tracer. Resource,
Configuration, Device, User et Instance, eux, passaient : ils sont concrets,
et c'est ce qui rendait le trou invisible.
Remplace par une remontee a la classe de base auditee (IsInstanceOfType).
EntityType porte « Section », pas « SectionMap ». Des deux sorties possibles,
c'est la normalisation cote serveur qui est retenue :
- le filtre « Section » est deja dans l'ecran et se met a rendre des lignes
sans toucher manager-app, donc sans coordonner deux repos ;
- elargir le filtre front aurait coute 13 entrees de liste et 39 cles i18n,
et surtout aurait laisse tout futur sous-type sortir du filtre en silence
-- la meme classe de panne que celle qu'on ferme ici ;
- le sous-type concret n'est pas perdu : le discriminateur TPH est une
propriete du modele, donc serialisee dans NewValues (« Discriminator »:
« Article »). Un test le tient.
8 tests, dont un par reflexion qui affirme que les 13 sous-types concrets
resolvent bien vers Section : la liste de types d'origine n'obligeait
personne a la suivre, c'est ce qui l'a laissee devenir fausse.
dotnet test 171/171.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La base semait Starter / Standard / Premium / Essentiel : les noms ET les
quotas avaient divergé de la grille de myinfomate-landing, sans que rien ne
le signale puisqu'aucun code n'indexe sur ces noms.
Écarts corrigés, pas seulement des renommages :
- Essentiel portait 500 000 jetons IA alors que la grille dit « Assistant IA
visiteur : non inclus ». Passé à 0.
- Standard → Pro : 10 GB au lieu des 15 GB vendus, et 5 M jetons alors que
l'IA n'est pas incluse non plus. Corrigé aux deux endroits.
- Essentiel et Pro avaient 395 jours de rétention de stats alors que la
grille annonce des stats basiques (30 jours).
- Premium était déjà conforme.
- plan-enterprise créé : stockage illimité, IA en quota sur mesure.
- plan-starter supprimé — aucun équivalent commercial, et son 0 jeton en
faisait un piège pour qui cherchait « le plan d'entrée ».
L'assistant IA n'étant inclus qu'à partir de Premium, il se vend en add-on
sur les plans inférieurs : surcharger Instance.AiTokensPerMonth suffit,
ApplyPlanQuotas ne repartant du plan qu'au changement de plan.
⚠️ La migration a été réécrite : EF avait scaffoldé les DeleteData en tête,
ce qui aurait buté sur la clé étrangère Instances.SubscriptionPlanId — ou
laissé des instances sans plan, donc à quotas nuls, donc sans IA ni
stockage. Les nouveaux plans sont insérés d'abord, les instances repointées
ensuite, les suppressions en dernier.
Sémantique du 0 laissée intacte et documentée : illimité pour le stockage,
pas d'IA pour les jetons. D'où long.MaxValue sur Enterprise.
dotnet build vert, dotnet test 143/143, migration appliquée en local.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LOT B — une seule migration EF (LotB_FreezeSchema) :
- SectionMap.MapResourceId → IconResourceId. L'écart (g) de la bascule tombe
avec. Il fallait renommer aussi la propriété de navigation MapResource :
la convention EF l'appariait au FK, la laisser aurait fabriqué un FK
fantôme. Elle n'était utilisée nulle part ailleurs.
- SectionEvent.ParcoursIds supprimé (champ, DTO, SectionFactory, et une
initialisation dans un montage de test).
- Instance.IsImageWatermark remplace le `instanceId == "633ee379…"` en dur
de ResourceController.
EF a généré un RenameColumn, pas un drop+add : les icônes déjà configurées
survivent. L'avertissement de perte de données ne porte que sur le DropColumn
de ParcoursIds, ce qui est l'intention.
Non fait, et c'était une erreur de doc : « supprimer SectionEvent.IconResourceId ».
Ce champ n'existe pas — la ligne visée appartient à la classe imbriquée
MapAnnotation, partagée par SectionEvent, SectionAgenda et SectionMap, lue par
cinq contrôleurs et par GetReferencedResourceIds. La supprimer aurait cassé
les icônes d'annotation des trois types et la collecte offline.
SÉCURITÉ (lot A, même repo) :
- AuthenticationController.Authenticate : un bloc #if DEBUG écrasait l'email
et le mot de passe reçus par un compte de test, donc toute compilation en
Debug authentifiait n'importe quelle saisie. Retiré.
- EnableSensitiveDataLogging (qui écrit les valeurs des paramètres dans les
logs) passe sous #if DEBUG, l'idiome déjà employé dans Startup.cs pour le
CORS et Hangfire. Le Dockerfile publiant en -c Release, c'est un verrou réel.
LOT C1 :
- Calculateur StoragePath/SizeBytes extrait dans Helpers/ResourceStorage.cs,
avec 13 tests fixant l'invariant des types URL. Il ferme le lien L5 : le
backfill (C2) et l'écart (e) de la migration appelleront le même code.
- L'extraction a révélé la divergence qu'elle devait empêcher : des deux
chemins de création de ResourceController, le chemin multipart écrivait
SizeBytes mais laissait StoragePath nul.
dotnet build Debug et Release verts, dotnet test 143/143 (130 + 13).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ingestion et indexation
- IIngestionService/IngestionService : chargement des collections filles par
sous-type, un jeu de morceaux par langue, ChunkIndex continu.
- SectionIndexingInterceptor retenu comme unique déclencheur : les 5
sous-contrôleurs totalisaient 30 SaveChanges et 0 Enqueue, donc ajouter des
points d'intérêt à une carte ne réindexait rien.
- HTML retiré avant l'embedding et lignes trop longues recoupées : sans cela un
article dépassait l'entrée max du modèle et emportait son lot de 50 morceaux.
- Gabarits de LanguageInit filtrés, DistinctBy(Text) avant le Take : ils
occupaient les cinq premiers résultats d'une recherche en néerlandais.
Endpoints du guide IA
- GET /api/Ai/knowledge/{id} : agrégats sur ContentEmbedding, donc sur ce qui
est réellement indexé — compter les sections publiées serait plus flatteur et faux.
- GET /api/Ai/insights/{id} : miroir de GuideIaInsights côté manager-app, c'est
l'écran qui a fixé la forme pour que le job de thèmes la remplisse.
RGPD
- VisitorQuestion journalisée dans AiController.Chat. HasAnswer se déduit des
sources du retrieval, pas du texte : un repli poli ressemble à une réponse.
L'écriture n'échoue jamais la réponse au visiteur.
- VisitorQuestionPurgeService, 90 jours, actif sans condition de configuration :
une durée écrite dans les CGU n'est pas un réglage commercial.
Corrections
- Updateinstance ne recopiait pas les quotas du nouveau plan.
- CheckQuota ne bloquait ni ne comptait à quota 0 — IA gratuite non comptée.
- StoragePath et SizeBytes renseignés à Create, types URL exclus.
dotnet build 0 erreur, dotnet test 130/130.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
75 fichiers, ~1 mois de travail depuis f222861 (17/07). Contenu :
Onboarding self-service — jamais exécuté de bout en bout
OnboardingController, StripeWebhookController, StripeService, ResendEmailService
+ IEmailService (10 templates), EmailTemplates/, TrialLifecycleService (Hangfire),
PasswordTokenHelper, SlugHelper, 5 DTOs. Essai 14 j, Stripe customer/Checkout/Tax,
mot de passe oublié + invitation user, plafond IA d'essai.
Schéma Postgres v3 — passe pendant que la base est vide
ContentEmbedding + index HNSW (vector_cosine_ops), IEmbeddingService +
GoogleEmbeddingService (gemini-embedding-001, 768 dims), Deployment/Dockerfile.postgres
(postgis 3.4.3 + pgvector 0.8.6, épinglé par digest — un tag mobile rejouerait le
warning de collation glibc). Colonnes Resource : StoragePath, FileName,
IncludeInAiKnowledge, AiIndexStatus + nouveaux ResourceType ajoutés EN FIN d'enum.
Guide IA
Champs Guide* sur Instance + InstanceDTO, AssistantService lit la configuration client
dans les 4 blocs de prompt (ton codé en dur retiré, règle hors-sujet ajoutée aux deux
variantes qui n'en avaient pas), IHttpClientFactory à la place des new HttpClient().
Table VisitorQuestion + ConversationId sur AiChatRequest.
Stats
Rétention unifiée à 13 mois (instances ET plans), VisitEventPurgeService.
Nettoyage
IsStepLocked / IsHiddenInitially / FactContent supprimés de GuidedStep — IsStepLocked
rendait une étape définitivement infranchissable même après réussite.
SectionMap allégé (-57 lignes).
Tests
SectionParcoursControllerTests, FakeConfiguration, FakeEmailService.
ContentEmbedding a cassé 116 tests sur 124 (EF InMemory ne connaît pas Vector) :
l'entité est exclue quand le provider n'est pas Npgsql. Conséquence assumée —
le vector store n'est couvert par aucun test. dotnet test 124/124.
10 migrations EF. Base locale à jour, dotnet build 0 erreur.
Rien n'est en prod : la bascule Mongo → Postgres est décrite dans DOCS/STATUS.md §1quinquies.
⚠️ appsettings.json contient les clés Stripe (test) et Resend (prod) en clair — à rotationner.