187 Commits

Author SHA1 Message Date
Thomas Fransolet
2dc0cfceeb Lot F : portail de facturation Stripe et coût réel d'une question
Customer Portal : StripeService.CreateBillingPortalSessionAsync +
POST /api/onboarding/billing-portal, calqué sur checkout-session.
Une garde absente de l'énoncé : StripeCustomerId peut être nul, car
CreateCustomerAsync n'est appelée que par l'inscription self-service.
Les 4 instances venues de Mongo n'ont jamais vu Stripe — sans la garde,
elles recevaient une erreur d'API Stripe illisible côté front. 409,
distinct du 404 d'instance inconnue.

Ratio jetons -> questions : InstanceQuotaDTO.aiTokensPerQuestion, mesuré
sur les VisitorQuestion.TokensUsed de l'instance, seuil de 20 questions
avant de faire foi — une seule réponse citant un long article doublerait
la moyenne. En dessous, repli sur l'hypothèse de la grille tarifaire
(10 000, pas 1 000 : le /1000 de manager-app était faux d'un facteur 10).

Trois autres points du lot F étaient déjà faits et n'attendaient qu'une
vérification : rate limiting, endpoint ApplicationInstance, quotas seed.

6 tests. dotnet test : 197 passés, 14 sautés, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 10:10:50 +02:00
Thomas Fransolet
a137cb582d Rate limiting sur les endpoints IA, partitionne par instance
La cle API publique d'une instance est embarquee dans les apps visiteur et
lisible en clair dans le navigateur sur visitapp-web : elle est publique par
construction. Les endpoints IA sont les seuls qui coutent de l'argent reel
(jetons Gemini), et rien n'empechait d'y boucler jusqu'a vider le quota
mensuel d'un client qui n'a rien fait.

AddRateLimiter natif .NET 8, fenetre fixe 120 req/min, 429 avec Retry-After.
Applique a chat ET translate : les deux consomment des jetons, et translate
est atteignable avec la meme cle.

Partition par instance parce que c'est l'instance qui porte le quota protege :
l'abus chez un client ne doit pas ralentir les autres.

Deux choix de placement qui ne sont pas cosmetiques. UseRateLimiter est apres
UseCors — un 429 pose avant les en-tetes CORS s'affiche comme une erreur CORS
et le client ne voit jamais le vrai code — et apres UseAuthentication, sinon
la partition n'a pas le claim d'instance et tout le monde tombe dans le meme
seau, ce qui transformerait la protection en panne globale.

Jamais exerce a l'execution : le projet n'a aucune infrastructure de test HTTP
et en monter une pour ce seul controle serait disproportionne. A verifier une
fois par une boucle de 130 appels, qui doit basculer en 429 au 121e.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 17:46:52 +02:00
Thomas Fransolet
fa92395d17 D2 : exposer DateUpdate sur ResourceDTO pour le filtre incremental
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>
2026-08-12 17:46:37 +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
2b1b20cd49 Le vector store est enfin eprouve a DEUX instances, sur un vrai Postgres
Le RAG n'avait tourne que sur une base a une seule instance -- le cas ou le
post-filtrage HNSW ne se voit pas. 14 tests Testcontainers sur l'image de
`Deployment/Dockerfile.postgres`, donc le meme PostGIS + pgvector que la
prod. Ils se sautent proprement (SkippableFact) sans demon Docker.

Ce que ca etablit :

- pgvector 0.8.6 : `SET hnsw.iterative_scan` est accepte. Ce SET est pose a
  chaque recherche par SearchAsync et n'existe qu'a partir de 0.8 -- sur une
  version anterieure, toute recherche echouait en production.
- Les extensions vector et postgis sont bien creees par les migrations.
- A deux instances, l'une saturant l'axe de la question a 30 contre 1,
  SearchAsync rend le bon nombre de resultats, tous de la bonne instance.
  Aucune fuite d'un client vers un autre.

⚠️ DEUX RESULTATS INATTENDUS, contraires a ce que le plan supposait.

1. Ce qui protege du post-filtrage n'est PAS le parcours iteratif, c'est
   l'index sur (InstanceId, ContentType). Le planificateur filtre par
   instance d'abord et trie exactement : l'index HNSW n'est jamais touche,
   donc il n'y a rien a post-filtrer. Verifie a 620 lignes (test) et a
   22 000 hors suite, avec 2 000 lignes pour l'instance cible. Un test fige
   ce plan -- si cet index disparaissait, la recherche se degraderait sans
   qu'aucune erreur ne le dise.

2. Et si on retire cet index, `hnsw.iterative_scan = relaxed_order` NE
   RATTRAPE RIEN : le parcours s'epuise apres ~335 lignes sans avoir atteint
   l'instance minoritaire, et rend zero. Le meme jeu de donnees avec l'index
   HNSW construit APRES l'insertion rend bien ses 20 lignes -- c'est donc la
   connectivite du graphe qui decide, et nos migrations creent l'index sur
   une table vide. Le SET de SearchAsync est donc une assurance qui ne couvre
   pas ce qu'on croyait ; on le garde (il est correct et sans cout), mais
   c'est l'index qui fait le travail.

Deux notes d'outillage :

- Testcontainers est epingle en 3.10.0 : la 4.x parle l'API Docker 1.44 et
  l'engine local plafonne a 1.43.
- L'image est construite par le CLI docker, pas par le constructeur d'images
  de Testcontainers : celui-ci relit le FROM pour pre-tirer l'image de base
  et ne sait pas parser `tag@sha256:`. Ce digest protege la base d'un
  changement de glibc sous ses index -- il ne se retire pas pour un test.

S'ajoutent 4 tests de GetSummary contre le vrai Postgres, qui levent le
caveat du commit precedent : le provider InMemory evalue tout cote client,
donc il ne disait rien de la traduction SQL. Les agregats du plan de base,
ceux du plan avance, le filtre appType et la fenetre vide traduisent tous.

dotnet test 200/200, aucun saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:34:56 +02:00
Thomas Fransolet
a3bf9523e3 GetSummary agrege en SQL au lieu de charger 13 mois en memoire
eventsQuery.ToList() ramenait la fenetre entiere, toutes colonnes comprises,
pour n'en faire que des comptages. La retention est passee de 30 jours a 13
mois (StatsRetentionDays = 395) : la methode charge donc desormais treize
fois ce pour quoi elle avait ete ecrite, et ca grossit tout seul.

Ce qui part en SQL : sessions distinctes, sommes de duree par session,
duree moyenne par section, top sections, visites par jour, distributions
par session, top articles, total des scans QR.

Ce qui ne peut pas y aller : POI, agenda, quiz, jeux, menus et validite des
QR se regroupent sur du JSON dans Metadata. On ne remonte plus que les deux
colonnes utiles, et seulement pour le type d'evenement concerne (MetadataOf).

Deux effets de bord voulus :

- les stats avancees ne sont plus calculees puis effacees pour les plans qui
  n'y ont pas droit -- les six requetes ne partent pas ;
- les titres de section ne sont resolus que pour les ids reellement affiches
  (top 10 + quiz), plus pour tous les ids vus dans la fenetre.

Un changement de semantique, delibere : « la langue de la session » etait
prise sur le premier evenement rendu par la base, dans un ordre indefini.
C'est desormais le plus ancien horodatage. Meme chose pour AppType.

Le reste est preserve a l'identique, y compris les cas tordus : un scan QR
sans metadata compte dans le total sans etre ni valide ni invalide, une
metadata illisible compte la completion de quiz a zero.

11 tests ajoutes -- la branche « stats avancees » n'etait couverte par
AUCUN test : les cas existants ne seedent pas d'Instance, donc
hasAdvancedStats etait toujours faux et la moitie de la methode n'etait
jamais executee. dotnet test 186/186.

⚠️ Ces tests tournent sur le provider InMemory, qui evalue tout cote client :
ils tiennent la semantique, pas la traduction SQL. La verification sur un
vrai Postgres arrive avec les tests Testcontainers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:04:39 +02:00
Thomas Fransolet
a3e0b4df71 Le plafond de 5 utilisateurs existe enfin cote serveur
CreateUser ne comptait rien. Ce qui a ete livre le 12/08 dans manager-app --
compteur « X / 5 » et bouton d'ajout desactive -- est un garde-fou
d'interface : un POST direct sur l'API passait toujours. Le controle est
desormais fait la ou il est opposable, et rend 422.

Cote front, rien a retoucher : le resultat d'invokeAPI est deja lu depuis le
correctif du 409 e-mail deja pris, donc le message remontera tel quel.

Deux choix, tous deux documentes dans le code :

- 5 en dur. Le faire varier par plan serait une colonne sur SubscriptionPlan,
  qui n'en porte aucune sur les utilisateurs -- donc une migration apres le
  gel du schema (lot B). Dette V1 assumee.
- Le SuperAdmin n'y est pas soumis. C'est la seule porte de service qui
  reste tant qu'aucun champ ne permet de relever la limite d'un client, et
  ca s'aligne sur le front, qui ne lui montre deja pas le compteur.

L'inscription self-service (OnboardingController) n'est pas concernee : elle
cree le premier utilisateur d'une instance neuve.

4 tests : au plafond, sous le plafond, plafond par instance, SuperAdmin
exempte. dotnet test 175/175.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 14:55:24 +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
269b3f6703 Lot C : backfill des colonnes de stockage (C2) et quota autoritaire (C3)
C2 — POST /api/Resource/backfill-storage, SuperAdmin, dryRun à true par
défaut : la migration se joue sur une base vide, ce backfill sur des lignes
de production. StoragePath par ResourceStorage.PathFor, SizeBytes par HEAD.

La méthode annoncée au plan — « SizeBytes par listing du bucket Firebase » —
était inapplicable : le serveur n'avait aucun client de stockage. Le sondage
passe donc par HEAD sur l'URL publique, comme le fait déjà la migration, et
le sondeur est extrait plutôt que recopié (Helpers/ResourceSizeProbe,
consommé par MigrationController et par le backfill). Même raisonnement que
pour ResourceStorage : deux copies auraient divergé sur ce qui compte, le
sort réservé aux échecs.

L'extraction a bouché un trou que personne ne cherchait. L'original ne notait
l'échec que dans son catch, or un HEAD sur un blob absent ne lève pas : il
répond 404, sans Content-Length. Ces ressources arrivaient à 0 octet sans
figurer dans le rapport — invisibles au quota et invisibles au diagnostic,
exactement ce que le commentaire d'origine voulait empêcher.

Le « 37 lignes sur 45 » du plan n'étant pas vérifiable, le backfill rend son
propre inventaire : Orphans (aucune URL, blob peut-être jamais téléversé) et
Unsized (URL présente, bucket muet) restent séparés, ce sont deux causes
distinctes.

C3 — pré-vol du quota sur les deux chemins de création, suppression du blob
à Delete, angle mort d'Update tranché.

Deux défauts trouvés en câblant, qui n'étaient documentés nulle part :

- Le pré-vol existait déjà à moitié. Upload (multipart) contrôlait et
  renvoyait 413, Create (JSON) ne contrôlait rien — or c'est le chemin
  qu'emprunte manager-app, qui crée la ligne puis téléverse.
- Les deux lectures du quota divergeaient. Upload lisait le quota du plan,
  GetQuota celui de l'instance avec le plan en repli. Une instance à quota
  surchargé — le mécanisme même de l'add-on — affichait un chiffre à l'écran
  et se faisait bloquer sur un autre. Helpers/StorageQuota devient la seule
  source de vérité pour les deux.

Delete supprime le blob AVANT la ligne et renvoie 502 en conservant la ligne
si le bucket échoue. manager-app faisait l'inverse en avalant l'échec dans un
print : la ligne disparaissait, le blob restait, et n'ayant plus de ligne il
devenait invisible au quota tout en restant facturé. Une ressource encore
listée se rattrape ; un blob que plus aucune ligne ne désigne, non.

L'angle mort laissé ouvert par C1 était une fausse crainte : PathFor ne
construit qu'un pictures/{instanceId}/{resourceId}, le type n'entre pas dans
le chemin, il décide seulement s'il y en a un. Recalculer ne peut donc pas
pointer ailleurs, et Update rejoue Apply.

Aucun secret nouveau : FirebaseAdmin était déjà référencé pour les
notifications push et Startup charge déjà un service account, donc
Google.Cloud.Storage.V1 réutilise le même GoogleCredential. Seule s'ajoute la
clé Firebase:StorageBucket, vide par défaut — à renseigner en prod (I9),
sans quoi Delete ne supprime rien et ne prétend pas le contraire.

dotnet build vert, dotnet test 163/163 (148 au départ, +7 pour C2, +8 pour C3).

Contient aussi le correctif d'indexation préparé en parallèle : un job
Hangfire par section dans BackfillInstanceAsync au lieu d'une boucle, et un
backoff sur 429/503 dans GoogleEmbeddingService.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 12:05:25 +02:00
Thomas Fransolet
f9059ed198 D1 : la visite hors ligne embarque enfin ses médias
Le switch de collecte des ressources était commenté : 157 lignes mortes dans
Export, 126 dans Import. Une visite téléchargée n'embarquait donc que l'image
de la configuration, celle du loader et l'image de chaque section — ni
contenus d'articles, ni audios, ni icônes de carte, ni images de quiz.

Export matérialise désormais les entités, pas seulement leurs DTO, et appelle
GetReferencedResourceIds(language) : la méthode existait déjà sur les 13
sous-types et n'attendait que d'être appelée. Elle filtre aussi les audios par
langue, ce que le switch ne faisait pas.

Import cesse de redécouvrir les ressources section par section : l'export
embarquant tout ce qui est référencé, une passe unique sur la charge suffit.
createResource étant idempotente, les quelques appels ciblés qui subsistent
sont sans effet.

4 tests, dont un qui vérifie par réflexion que les 13 sous-types implémentent
la collecte : le trou venait d'un switch où un type oublié passait dans le
default sans bruit, et c'est ce qu'il ne faut plus pouvoir refaire.

dotnet test 148/148.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:57:22 +02:00
Thomas Fransolet
149ebc0cf1 Dry run de la bascule, et les 20 sections qui disparaissaient en silence
Le dry run est automatisé en test (MigrationDryRunTests) : il appelle le
contrôleur directement, ce qui évite la chaîne login + JWT SuperAdmin, et se
saute si aucun Mongo n'écoute sur MIGRATION_TEST_MONGO — la suite reste donc
verte sans dépendance. Les tailles de fichier sont court-circuitées : un HEAD
sur 2374 ressources prendrait des minutes pour une information que ce test ne
vérifie pas.

Il a trouvé du premier coup ce qu'aucune lecture de code n'avait vu : 327
sections dans Mongo, 307 migrées, et Erreurs : 0.

Les 20 manquantes référencent deux configurations supprimées dans Mongo — 19
articles et un slider, du contenu MDLF. La migration a raison de ne pas les
reprendre : les sections sont collectées configuration par configuration,
donc les orphelines ne sont jamais énumérées, et la FK ConfigurationId les
refuserait. Le défaut était le silence — elles partent désormais dans
Skipped, avec le détail de la configuration introuvable.

L'invariant affirmé par le test n'est donc pas « tout est migré » mais
« rien ne disparaît sans être signalé » : migrées + signalées = Mongo.

dotnet test 144/144.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:40:11 +02:00
Thomas Fransolet
934d71c2ee Lot G : les 8 écarts de MigrationController
Trois n'existaient pas — vérifié dans l'export Mongo plutôt que dans la doc :
- (a) « BuildSection ne couvre que 11 types sur 13 » : les 327 sections de
  l'export ne contiennent que les types 0 à 10. SectionEvent et
  SectionParcours sont nés avec Postgres v3, le default est un filet.
- (f) IsQRCode/IsSearchText/IsSearchNumber n'existent pas dans Mongo, false
  est le bon défaut. Contrôle inverse fait : les cinq réglages qui existent
  vraiment sont bien mappés sur AppConfigurationLink.
- (g) était tombé avec le rename du lot B.

Les cinq réels :
- (b) QuizQuestions = new() jetait les questions. Mesuré : 5 sections quiz
  en portent 41, réponses comprises. Migrées en MultipleChoice, le type de
  validation n'existant pas dans l'ancien modèle. EventAgendas et l'absence
  de GuidedPath sont corrects : Mongo n'a ni événement ni parcours.
- (c) la source ne contient que 4 champs, il n'y avait rien à mapper de
  plus. PublicApiKey générée (même schéma que l'onboarding), WebSlug via
  SlugHelper, drapeaux de plateforme dérivés des configurations, plan
  affecté par nom avec repli sur Pro, IsAssistant allumé seulement si le
  plan donne des jetons.
- (d) Mongo n'a pas de champ Role : le ContentEditor en dur dégradait les
  10 utilisateurs en silence, plus personne n'aurait pu gérer les
  utilisateurs de son instance. Passé à InstanceAdmin.
- (e) StoragePath n'était pas écrit du tout. Passe par ResourceStorage, le
  calculateur commun à la création et au backfill. L'échec du HEAD, jusqu'ici
  avalé, remonte dans le rapport.
- (h) transaction unique sur les neuf étapes, et 500 au lieu de 200 OK sur
  échec fatal — un script de bascule testant le code HTTP concluait au
  succès alors que l'erreur n'était que dans le corps.

ApplyPlanQuotas est dupliqué depuis InstanceController plutôt qu'appelé : la
migration ne doit pas casser si ce contrôleur change de forme.

dotnet build vert, dotnet test 143/143. Reste le dry run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:26:01 +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
7625487806 Lot B : geler le schéma, plus sécurité rapide et calculateur de stockage
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>
2026-08-11 15:32:19 +02:00
Thomas Fransolet
18e4240f0f RAG: pipeline d'ingestion, endpoints du guide IA, journalisation RGPD
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>
2026-08-11 10:48:10 +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
19a0157b19 Bento support + upgrade security (filter by instance mandatory) 2026-07-14 17:21:10 +02:00
Thomas Fransolet
3cd5f7573d misc (ia assistant, SectionParcours + others fixs) 2026-07-08 17:00:43 +02:00
Thomas Fransolet
674655eb80 Update agenda sync (image management) + update isStatistics to hasStats (subscription plan) 2026-05-07 22:00:42 +02:00
Thomas Fransolet
9633ae43b7 Add support for web + misc 2026-05-07 16:49:56 +02:00
Thomas Fransolet
00f702b3da Update for quotas (use it in ia translator) 2026-04-10 17:04:46 +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
09b7c75dac WeatherSync service (to test) 2026-03-25 17:38:44 +01: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
bde1666b42 Update save geometry + other, id null etc (thx claude) 2026-03-06 15:22:02 +01:00
Thomas Fransolet
f17d72b34e Multiple fixs after help from ai 2026-03-04 16:36:13 +01:00
Thomas Fransolet
98f3606887 Fix geopoint doesn't working 2025-11-27 16:52:10 +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
8f16f1570f Added method to return all section from one type + misc 2025-09-18 23:23:36 +02:00
Thomas Fransolet
fd191ec647 Moved guided stuffs 2025-08-20 14:35:37 +02:00
Thomas Fransolet
cbf3a3c7f5 Misc 2025-08-14 23:24:30 +02:00
Thomas Fransolet
4d755549af Updated code for device creation (create appconfig) + misc 2025-08-14 21:31:26 +02:00
Thomas Fransolet
b81972f723 Updated for kiosk (can assign different config on different device) 2025-08-14 16:56:02 +02:00
Thomas Fransolet
c8c5902292 misc + fixs 2025-08-14 16:30:03 +02:00
Thomas Fransolet
5aabf35f9d Add isweb to instance 2025-08-01 16:24:01 +02:00
Thomas Fransolet
b3b6a5d6fc Fix fromdto methods + misc 2025-07-17 11:58:23 +02:00
Thomas Fransolet
b5396643ce to lowerCase in all DTO + update to GeometryDTO 2025-07-16 15:28:54 +02:00
Thomas Fransolet
62e302a0f0 Update code for event, agenda and map controllers -> added DTOs + misc 2025-07-16 14:57:13 +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
c89566c9a9 Added link EventAgenda and SectionEvent + event agenda dto 2025-07-10 17:08:08 +02:00
Thomas Fransolet
8801c56f75 Added EventAgenda 2025-07-10 16:43:29 +02:00
Thomas Fransolet
400084fad5 Added explaination SectionMap, guidedPath etc 2025-07-09 17:44:48 +02:00