feat(gating): multi-profils (Base+) non destructif #306
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#306
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-300-multi-profile-gate"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Gate multi-profils (Base+), NON destructif — 3e maillon de la pile feature-gating (base = issue-299-routes-sidebar).
ready && !allowed, anti-flash) ; le clic ouvre un dialog upsell (UpsellGate réutilisé dans un overlay, prop additiveonNavigatepour fermer le dialog après « J'ai déjà une clé »). Le profil actif reste pleinement accessible.createProfile), couvre les 2 entrées (ProfileSwitcher ET ProfileSelectionPage) : bouton « Créer » visible avec cadenas, clic → panneau upsell compact (mêmes clésupsell.*; pas UpsellGate car le modal s'ouvre aussi depuis ProfileSelectionPage, rendue HORS BrowserRouter →useNavigatecrasherait). Race guard danshandleSave(fenêtre de boot licence).src/shared/profileGate.ts(isProfileSwitchLocked,isProfileCreationLocked) + 10 tests (matrice ready/allowed/actif/compte, création du 1er profil jamais bloquée).profiles.jsonintact ; un upgrade Base/Premium fait tout réapparaître sans migration.upsell.*,nav.locked,license.editions.*) ; CHANGELOG centralisé dans #302.Tests : 871 vitest verts (861 baseline + 10) ;
npm run build(tsc + vite) propre.Resolves #300
Generated autonomously by /autopilot run of 2026-07-20
Review adversariale — APPROVE
Résumé : Gate multi-profils (Base+) correctement implémenté, non destructif par construction — les prédicats purs (
profileGate.ts) ne décident que du verrouillage UI, aucun code de suppression,profiles.jsonintact. Chaque affirmation du body vérifie contre le code (lecturegit show, sans checkout).Points vérifiés
createProfilen'a qu'un seul call site UI (ProfileFormModal.tsx:78) ; aucun chemin parallèle viasaveProfiles. Gater le modal couvre bien les 2 entrées (ProfileSwitcher et ProfileSelectionPage).LicenseProviderest monté au-dessus deAppdansmain.tsx, doncuseEntitlementfonctionne sur ProfileSelectionPage (rendue hors BrowserRouter) ; leuseNavigateinconditionnel d'UpsellGate y crasherait — le panneau compact est justifié.Sidebar.tsx:47appliqueif (!ready || allowed) return null; les prédicats suivent la même règle (if (!gate.ready || gate.allowed) return false).pinProfilen'est set qu'après le checkisLocked— le PIN path est inatteignable sur une row verrouillée. Le remplacement du cadenas PIN par le cadenas gating sur les rows verrouillées est documenté.activeProfileId === nullne verrouille rien (never-lock-out), 1er profil jamais bloqué,!readyne verrouille rien, race guardhandleSave(fenêtre de boot licence). Gestion des profils existants (rename/PIN/delete) intacte.upsell.*,nav.locked,license.editions.*,profile.create,common.cancel) existent toutes en FR et EN ; zéro texte en dur, zéro nouvelle clé comme annoncé.Resolves #300présent.issue-299-routes-sidebar) →no-statusattendu ; valider le tip cumulé en local avant merge (gotcha connu).Suggestions (non bloquantes)
ProfileFormModalduplique le markup CTA d'UpsellGate. Une extraction futureUpsellPanel(présentational, router-free) queUpsellGatewrapperait éliminerait la duplication tout en respectant la contrainte hors-router.profileGate.test.ts:36— le libellé « (allowed short-circuits) » est inexact : dansif (!gate.ready || gate.allowed), c'est!readyqui est évalué en premier pour{allowed: true, ready: false}. L'assertion reste correcte.ProfileFormModal.tsx:188) : le label PIN affiche({t("common.cancel").toLowerCase()})→ « (annuler) » là où on attend vraisemblablement « (optionnel) ». À corriger hors pile.Mergé en fast-forward sur main (pile #304→#308), voir commit
b89074e. Verdict /pr-review : APPROVE. Fermeture manuelle : le merge local ff n est pas détecté comme merged par Forgejo.Pull request closed