Crédits Studio : un préavis, pas un nouveau plafond (décision 27)

La limite demandée existe déjà deux fois — le solde prépayé est le plafond, et
StudioPerUserDailyCap borne la journée d'un utilisateur. Ce qui manque est
l'alerte : aujourd'hui le premier signal reçu est une génération qui échoue en
InsufficientCredits, et l'expiration à 12 mois fait s'évaporer un solde sans
prévenir. Seuil configurable par instance, mail au franchissement et rappel
d'expiration à J-30.

Carte kanban 282.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thomas Fransolet 2026-09-16 15:29:16 +02:00
parent fb1d15d0b3
commit 125813c34e
2 changed files with 14 additions and 0 deletions

View File

@ -0,0 +1,13 @@
---
title: Seuil d'alerte sur le solde de crédits Studio (pas un nouveau plafond)
area: backend manager
horizon: v2
tags: studio, crédits
flag: warn | Nouveau 16/09 · le plafond dur a été retiré exprès (décision 22)
src: studio-plan.md décisions 17 et 22 · StudioCreditService.cs · studio_credits_section.dart
---
<p><strong>La limite demandée existe déjà, deux fois.</strong> Avec des crédits prépayés (décision 17), <strong>le solde <em>est</em> le plafond</strong> — c'est exactement pour ça que la décision 22 a retiré le plafond dur organisation le 13/09. Et le garde-fou contre le stagiaire qui vide le budget en une après-midi, c'est <code>Instance.StudioPerUserDailyCap</code>, déjà appliqué dans <code>StudioCreditService.ReserveAsync</code> (<code>CreditError.DailyCapReached</code>). Rien à ajouter de ce côté : <strong>ne pas recréer un plafond organisation.</strong></p>
<p>⚠️ <strong>Le modèle n'est pas celui de Google AI Studio ni de fal.ai.</strong> Là-bas on est en <strong>postpayé</strong> : sans budget alert ni hard cap, on découvre la facture. Ici le client a payé avant ; il ne peut pas se faire surprendre par une dépense, seulement par une <strong>indisponibilité</strong> — et ça, ça ne se règle pas avec un plafond, mais avec un préavis.</p>
<p><strong>Ce qui manque vraiment : le préavis.</strong> Aujourd'hui aucune alerte n'existe. <code>studio_credits_section.dart</code> affiche disponible / solde / réservé / expiration, mais il faut aller ouvrir l'écran Facturation pour les voir ; une génération qui échoue en <code>InsufficientCredits</code> est le premier signal reçu par le conservateur. Et surtout, <strong>l'expiration à 12 mois (décision 17) est le vrai piège</strong> : un solde qui s'évapore sans prévenir, sur une institution publique, c'est un litige — pas une notification manquée.</p>
<p><strong>À faire</strong> : <code>Instance.StudioCreditsAlertThreshold</code> (en crédits, 0 = désactivé) + <code>StudioCreditsAlertSentAt</code> pour ne pas renvoyer le mail à chaque génération ; déclenchement dans <code>StudioCreditService.Record</code> quand le solde passe sous le seuil, et dans <code>ExpireDueAsync</code> (déjà un job quotidien) pour un rappel d'expiration <strong>à J-30</strong>. Envoi par <code>IEmailService</code> / <code>ResendEmailService</code>, déjà en place. Côté manager-app : un champ dans l'écran Facturation et un bandeau dans le popover de la jauge du pied de menu, i18n FR/EN/NL.</p>
<p><strong>Petit lot</strong> : 2 colonnes, un hook, un template de mail, un champ d'UI. À faire <em>après</em> la fixation des prix (décision 16, fin du lot 3) — un seuil en crédits n'a pas de valeur par défaut tant que l'unité de crédit n'est pas arrêtée.</p>

View File

@ -162,6 +162,7 @@ Tout sur `Instance`, dupliqué depuis `SubscriptionPlan` : `StorageQuotaBytes`,
| 23 | Écarts des lots 2 à 4 — *13/09* | **Fournisseur interchangeable** : `IGenerationProvider` (soumission, sondage, lecture du webhook) + `GenerationModel.ProviderKey` en base ; changer de fournisseur = une classe et une ligne de catalogue. **Une requête par variante** (FLUX.2 pro n'a pas de `num_images`). **Référence source avant celles de l'identité** dans l'ordre d'envoi. Migration unique `AddStudioGeneration` pour les lots 2 et 3. **Non livrés** : `preview`, `GET /jobs`, `cancel`, `reject`, `regenerate`, l'interrupteur UI de `CanValidateAssets`. **Pas de `target` serveur** : l'onglet « Générer » vit dans le sélecteur (`showSelectResourceModal`, mode sélection) et la ressource validée revient au champ qui l'a ouvert — donc absent du bouton « Ajouter » de la Médiathèque, qui relève du lot 5. Surcharges d'identité en puces, pas en onglets ; `ModelKey` non exposé (un seul modèle). **`GuidedStep.ImageUrl` conservée** et remplie par le client avec `ImageResourceId` (motif `GeoPoint`) : visitapp-web n'a rien à changer, mymuseum-visitapp lit l'id pour l'hors-ligne, tablet-app n'a pas d'étapes. `CreditCost = 10` provisoire jusqu'au calibrage || 24 | Modèles 3D et multi-vues — *15/09* | **Tout reste sur fal** : Meshy (v5, v6-preview, v7), Hi3D, Tripo3D, Hunyuan3D et Trellis y sont exposés — aucun second fournisseur à intégrer, aucune seconde clé. **Deux lignes de catalogue, pas une** : `3d-default` mono-image (~0,020,05 $) et `3d-multiview` = `meshy/v7/multi-image-to-3d` (~1,20 $). Le facteur ~25 sur le coût impose que le basculement de l'un à l'autre soit **annoncé avant la génération, jamais silencieux**. Hi3D n'expose pas de multiview : excellent en fidélité structurelle, il ne couvre pas le cas d'usage central. Ids et prix à revérifier au calibrage (§9.4) | | 23 | Écarts des lots 2 à 4 — *13/09* | **Fournisseur interchangeable** : `IGenerationProvider` (soumission, sondage, lecture du webhook) + `GenerationModel.ProviderKey` en base ; changer de fournisseur = une classe et une ligne de catalogue. **Une requête par variante** (FLUX.2 pro n'a pas de `num_images`). **Référence source avant celles de l'identité** dans l'ordre d'envoi. Migration unique `AddStudioGeneration` pour les lots 2 et 3. **Non livrés** : `preview`, `GET /jobs`, `cancel`, `reject`, `regenerate`, l'interrupteur UI de `CanValidateAssets`. **Pas de `target` serveur** : l'onglet « Générer » vit dans le sélecteur (`showSelectResourceModal`, mode sélection) et la ressource validée revient au champ qui l'a ouvert — donc absent du bouton « Ajouter » de la Médiathèque, qui relève du lot 5. Surcharges d'identité en puces, pas en onglets ; `ModelKey` non exposé (un seul modèle). **`GuidedStep.ImageUrl` conservée** et remplie par le client avec `ImageResourceId` (motif `GeoPoint`) : visitapp-web n'a rien à changer, mymuseum-visitapp lit l'id pour l'hors-ligne, tablet-app n'a pas d'étapes. `CreditCost = 10` provisoire jusqu'au calibrage || 24 | Modèles 3D et multi-vues — *15/09* | **Tout reste sur fal** : Meshy (v5, v6-preview, v7), Hi3D, Tripo3D, Hunyuan3D et Trellis y sont exposés — aucun second fournisseur à intégrer, aucune seconde clé. **Deux lignes de catalogue, pas une** : `3d-default` mono-image (~0,020,05 $) et `3d-multiview` = `meshy/v7/multi-image-to-3d` (~1,20 $). Le facteur ~25 sur le coût impose que le basculement de l'un à l'autre soit **annoncé avant la génération, jamais silencieux**. Hi3D n'expose pas de multiview : excellent en fidélité structurelle, il ne couvre pas le cas d'usage central. Ids et prix à revérifier au calibrage (§9.4) |
| 25 | Casting visiteur — *15/09* | **Retenu** — lève le « hors périmètre » de §3.8. `ConfigurationPersona { ConfigurationId, PersonaId, Order, IsFeatured }` ne porte **que** l'ordre et la mise en avant : l'appartenance reste **calculée** depuis les narrateurs assignés. Un personnage ne s'assigne qu'à **un seul endroit — le contenu** ; l'écran casting est une *vue*, jamais une seconde saisie. Rien ne s'affiche côté visiteur sans `Configuration.IsCastingShownToVisitor` (défaut off). Lot 8bis | | 25 | Casting visiteur — *15/09* | **Retenu** — lève le « hors périmètre » de §3.8. `ConfigurationPersona { ConfigurationId, PersonaId, Order, IsFeatured }` ne porte **que** l'ordre et la mise en avant : l'appartenance reste **calculée** depuis les narrateurs assignés. Un personnage ne s'assigne qu'à **un seul endroit — le contenu** ; l'écran casting est une *vue*, jamais une seconde saisie. Rien ne s'affiche côté visiteur sans `Configuration.IsCastingShownToVisitor` (défaut off). Lot 8bis |
| 26 | Prise de vue mobile — *16/09* | **Page de dépôt dédiée ouverte par QR code, pas un manager-app responsive.** Le conservateur est devant la vitrine avec son téléphone, pas devant son poste ; rendre responsive un back-office Flutter web pour un seul écran revient à le rendre responsive tout court — refus déjà posé sur le routing (décision 23) — et CanvasKit se charge mal sur la 4G d'une salle. La page vit sur **la même surface web non-Flutter que l'éditeur de POI** (décision n°9 d'[immersif-frontiere-plan.md](immersif-frontiere-plan.md)) : aucune nouvelle cible de déploiement. Capture par `<input type="file" accept="image/*" capture="environment">`, qui ouvre l'appareil photo natif — **jamais `getUserMedia`**, dont le flux est sans autofocus ni traitement constructeur, alors que c'est le seul gabarit du Studio où la qualité des photos pèse plus lourd que le prompt. Accès par **token de dépôt** borné (§3.7bis), jamais un JWT manager. **Lot 11bis, après le lot 11** : le dépôt de fichiers depuis le poste couvre déjà le besoin fonctionnel | | 26 | Prise de vue mobile — *16/09* | **Page de dépôt dédiée ouverte par QR code, pas un manager-app responsive.** Le conservateur est devant la vitrine avec son téléphone, pas devant son poste ; rendre responsive un back-office Flutter web pour un seul écran revient à le rendre responsive tout court — refus déjà posé sur le routing (décision 23) — et CanvasKit se charge mal sur la 4G d'une salle. La page vit sur **la même surface web non-Flutter que l'éditeur de POI** (décision n°9 d'[immersif-frontiere-plan.md](immersif-frontiere-plan.md)) : aucune nouvelle cible de déploiement. Capture par `<input type="file" accept="image/*" capture="environment">`, qui ouvre l'appareil photo natif — **jamais `getUserMedia`**, dont le flux est sans autofocus ni traitement constructeur, alors que c'est le seul gabarit du Studio où la qualité des photos pèse plus lourd que le prompt. Accès par **token de dépôt** borné (§3.7bis), jamais un JWT manager. **Lot 11bis, après le lot 11** : le dépôt de fichiers depuis le poste couvre déjà le besoin fonctionnel |
| 27 | Alerte de solde, pas de nouveau plafond — *16/09* | **Le plafond reste retiré** (décision 22) : avec des crédits prépayés, le solde *est* le plafond, et `StudioPerUserDailyCap` couvre déjà le budget vidé en une après-midi. Ce qui manque est le **préavis** — le modèle n'est pas celui de Google AI Studio ni de fal.ai, **postpayés**, où l'alerte protège d'une facture ; ici elle protège d'une **indisponibilité**. On ajoute `Instance.StudioCreditsAlertThreshold` (0 = désactivé) + `StudioCreditsAlertSentAt` (anti-répétition), déclenchés dans `StudioCreditService.Record` au franchissement du seuil et dans `ExpireDueAsync` pour un rappel **d'expiration à J-30** — c'est le vrai risque de la décision 17, un solde qui s'évapore en silence chez une institution publique. Envoi par `IEmailService` (Resend, déjà en place), champ dans l'écran Facturation et bandeau dans le popover de la jauge. **Après la décision 16** : un seuil en crédits n'a pas de valeur par défaut tant que l'unité n'est pas fixée |
--- ---