25 Commits

Author SHA1 Message Date
Thomas Fransolet
b87e49d808 Cle d'API sur les routes visiteur, canal VR et contenu immersif
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>
2026-09-13 16:54:10 +02:00
Thomas Fransolet
4e17c572ba Nom d'app, drapeau QR et liens stores par application
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>
2026-09-10 11:42:07 +02:00
Thomas Fransolet
bd484db48a Lot J : agrégats de thèmes, job de regroupement, interrupteur de collecte
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>
2026-08-13 15:58:35 +02:00
Thomas Fransolet
7a80766d43 Le journal d'audit ne suit plus les ecritures de la machine
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>
2026-08-12 16:01:01 +02:00
Thomas Fransolet
805ca5cf3d Le journal d'audit voyait tout sauf le contenu
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>
2026-08-12 14:53:21 +02:00
Thomas Fransolet
a80096d72b Aligne les plans d'abonnement sur la grille de tarifs
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>
2026-08-11 16:06:36 +02:00
Thomas Fransolet
af6b5b76ef Self-service onboarding (Stripe + Resend) + Postgres v3 schema (pgvector, media, visitor questions)
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.
2026-08-09 22:11:48 +02:00
Thomas Fransolet
f2228617ca Add instance index + date update audit + ai quota to token 2026-07-17 15:21:51 +02:00
Thomas Fransolet
3cd5f7573d misc (ia assistant, SectionParcours + others fixs) 2026-07-08 17:00:43 +02:00
Thomas Fransolet
e0ff0eeba6 Quota update in controller, audit log (serilog) + ai translator + Unit tests ! (to be tested) + migration update 2026-04-10 16:28:33 +02:00
Thomas Fransolet
eff4f7ba5c Add seed subscriptionplan + wip migration controller 2026-04-01 17:26:27 +02:00
Thomas Fransolet
f72d94f30f Subscription plans 2026-04-01 17:00:13 +02:00
Thomas Fransolet
d5353eea9c wip service, fix some assistant ia bug, wip section agenda update, section event annotation etc 2026-03-25 10:56:54 +01:00
Thomas Fransolet
bad25bf5b3 Add push notification 2026-03-17 09:10:56 +01:00
Thomas Fransolet
a452f4af04 Add migration, need to check if migration with apikey correct.. ! + add roles and security (role and apikey) - need to be tested + visitevent stats + ai etc + all need to be tested ! 2026-03-13 17:45:01 +01:00
Thomas Fransolet
94443fa411 Update puzzle to game + misc 2025-11-27 16:19:00 +01:00
Thomas Fransolet
e70b574091 Updated applicationInstance for sectionEvent in dto 2025-10-17 23:21:10 +02:00
Thomas Fransolet
a0aaf6e601 ApplicationInstance done but not tested + SectionEvent done but to be tested + wip sectioncontroller update + misc 2025-07-15 17:31:39 +02:00
Thomas Fransolet
9671361a34 Update DB QuizQuestion and misc 2025-07-15 16:30:01 +02:00
Thomas Fransolet
a051ae5f70 list of coordinates to geometry support (for sql postgis support) 2025-07-10 21:57:54 +02:00
Thomas Fransolet
d241720102 Breaking changes ! WIP new format (ApplicationInstance) + SectionEvent and updated SectionMap 2025-07-09 17:37:30 +02:00
Thomas Fransolet
f4919801b7 Misc 2025-05-13 17:18:59 +02:00
Thomas Fransolet
cef69a61df section (to be tested when front) + section map controller + section quiz controller + update useless entities to DTO 2025-03-24 17:18:59 +01:00
Thomas Fransolet
11cfeab210 Updated DB (wip) 2025-03-19 08:57:29 +01:00
Thomas Fransolet
8c18825c5d Wip clean code and pass by postgres ! 2025-03-06 17:39:08 +01:00