Les jetons MCP sont désormais chiffrés (Fernet dérivée de SECRET_KEY) en plus
du hash conservé pour l'auth, permettant leur ré-affichage/copie à tout moment.
Getter scopé au propriétaire, dégradation propre si clé absente/changée.
Migration 0013 + colonne token_enc. UI alignée sur « Copier le lien » des vues.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Championnat mensuel (fenêtre glissante sur created_at), recharge des
votes le lundi (Europe/Paris) sans report, pas de cap par feature, et
précondition de déploiement gatée sur le nombre de votants.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le label du checklist CGU mélangeait des chaînes brutes et un composant
html.A dans une liste, ce que le rendu compound-label de dcc.Checklist
ne supporte pas (chaque item passe par ExternalWrapper qui suppose un
vrai composant Dash). Ça crashait en boucle et déclenchait des dizaines
de re-render, ce qui faisait aussi clignoter le lien Abonnement dans le
menu latéral. Fix : tout regrouper dans un seul html.Span.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Dropdown « Mes vues » : ajoute en tête un lien « Gérer mes vues » (couleur
primary) vers /compte/vues, séparé de la liste des vues.
- Corrige l'override de --bs-dropdown-link-hover-bg : Bootstrap 5.3 scope cette
variable sur .dropdown-menu, donc un override sur :root est masqué. Déplacé
sur .dropdown-menu.
Alphabet du jeton passé de base62 (a-z A-Z 0-9) à base36 (a-z 0-9), longueur
inchangée (6). Lien plus lisible/dictable et cohérent avec le slug déjà en
minuscules. Les jetons déjà générés (à casse mixte) restent valides (lookup par
match exact).
- Bouton de copie explicite (icône copy.svg + « Copier le lien » + retour
« ✓ Copié ») sur /tableau et /compte/vues, via dcc.Clipboard (children /
copied_children), à la place de l'icône seule.
- Le lien direct s'affiche en texte (span#share-url-text) plutôt qu'un
<input> : évite la troncature d'affichage du champ Bootstrap form-control en
flex (zone de texte bloquée ~218px), montre tout le lien avec retour à la
ligne, et prend la largeur du lien. Stylé comme un champ (bordure, coins
arrondis, texte 90%).
- Instructions /compte/vues simplifiées ; E2E adapté (#share-url-text).
Le datasource _get_rows des grilles acheteur/titulaire mélangeait une sortie
MATCH (getRowsResponse de la grille pattern-matching) avec deux sorties fixes
(<org>-total / -total-unique). Dash l'interdit : le dev-renderer (mode debug de
run.py) lève « MATCH wildcards must be on the same keys for all Outputs » (4
erreurs : 2 par page). Le callback échoue au dispatch et la grille reste vide.
Correctif : _get_rows n'a plus qu'une sortie MATCH (getRowsResponse) ; les
stores totaux (id fixes) sont alimentés impérativement via dash.set_props.
Test de régression en debug=True (dev-renderer) : sans le correctif la grille ne
charge aucune ligne ; avec, lignes + compteur « N marchés » présents.
La barre des vues sauvegardées restait masquée pour les non-abonnés via un
style inline display:none — inopérant car .d-inline-flex est
display:inline-flex !important, la barre était donc toujours visible.
La barre reste désormais visible pour tous ; « Sauvegarder la vue » et
« Mes vues » sont grisés et désactivés (disabled) pour les non-abonnés. Le
gating serveur de save_view (prepare_view_to_save) reste inchangé.
Remplace le verrou de dérive à compteur (suppress-next) par une détection
événementielle : eventListeners AG Grid (filterChanged/sortChanged) →
dashAgGridFunctions.hideShareOnUserAction, qui efface active-view sur action
utilisateur (source != 'api') et ignore l'écho de l'application programmatique.
Le compteur suppress-next souffrait d'une course d'ordonnancement Dash (l'écho
lisait la valeur pré-batch) et le diff de columnState était impraticable (état
réémis bruité). La visibilité du bloc bascule via la classe (d-flex/d-none) et
non le style inline, car .d-flex est display:flex !important.
Task 7 : test E2E de bout en bout (application + masquage sur action).
Ajoute les migrations 0008-0011 (oauth_clients, oauth_codes,
oauth_tokens, mcp_usage) et câble le serveur d'autorisation OAuth 2.1
dans le bloc DASH_MCP_ENABLED de app.py : init des schémas oauth/usage,
purge périodique, enregistrement des routes /oauth et /.well-known, et
exemption CSRF de ces routes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le garde before_request de /_mcp accepte désormais les jetons OAuth
opaques (store.get_token_by_access) en plus des jetons statiques
colibre_*, avec vérification d'audience (resource == mcp_resource)
et d'expiration. Le 401 porte resource_metadata pour le découverte
RFC 9728. Sur succès, incrémente le compteur du bon store et
journalise dans mcp_usage (best-effort).
Corrige aussi une régression d'assertion stricte sur le header
WWW-Authenticate dans test_app_wiring.py, cassée par le nouveau
suffixe resource_metadata.
Remplace le stub 501 de src/mcp/oauth/authorize.py par le flux réel
authlib (GET consentement, POST émission de code, échange de token),
avec gate d'abonnement avant tout affichage du consentement.
Deux ajustements de compatibilité authlib 1.7.2 dans server.py,
découverts en exécutant le flux bout-en-bout pour la première fois :
AUTHLIB_INSECURE_TRANSPORT en mode DEVELOPMENT (le client de test
Flask ne sert pas en HTTPS) et OAUTH2_REFRESH_TOKEN_GENERATOR (off
par défaut côté Flask, requis pour émettre les refresh_token du scope
offline_access). Détails dans .superpowers/sdd/task-9-report.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AuthorizationCodeGrant avec PKCE requis (CodeChallenge), RefreshTokenGrant
avec rotation + re-vérification d'abonnement, DCR (ClientRegistrationEndpoint)
et révocation (RevocationEndpoint) câblés sur le store sqlite (Tasks 1-3),
metadata (Task 5) et consent.subscription_ok (Task 6).
- _Token (dict + check_client/get_scope) fait le pont entre les rows sqlite
bruts et le protocole TokenMixin qu'authlib 1.7.2 attend pour le refresh
et la révocation.