Skip to content

fix(ui): la barre de progression des engagements peignait toujours 100 % - #297

Open
thierryvm wants to merge 1 commit into
mainfrom
fix/csp-barre-progression
Open

fix(ui): la barre de progression des engagements peignait toujours 100 %#297
thierryvm wants to merge 1 commit into
mainfrom
fix/csp-barre-progression

Conversation

@thierryvm

@thierryvm thierryvm commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Le défaut

La barre de progression de /app/commitments peignait 100 % quelle que soit la valeur. Sur un plan à 4 échéances sur 11, elle disait « soldé ».

Ce n'est pas cosmétique : c'est un chiffre faux dans une application de finances. L'utilisateur regarde cette barre pour savoir où il en est de son plan de paiement.

Mesuré, pas déduit

Chromium, npm run dev, stack Supabase locale, plan à 4/11 :

valeur
aria-valuenow 36
attribut style du remplissage width:36% ✅ présent
largeur calculée 1054 px sur une piste de 1054 px

src/proxy.ts:60 : style-src: 'self' 'nonce-…' — et en production, sans 'unsafe-inline' du tout (l'extra n'est ajouté qu'en dev, où le navigateur l'ignore de toute façon puisqu'un nonce est présent). Un nonce annule 'unsafe-inline', et faute de style-src-attr la politique gouverne aussi les attributs style. La largeur était donc jetée à l'analyse du document, et le div gardait sa largeur naturelle : 100 %.

Le lecteur d'écran recevait la bonne valeur via aria-valuenow pendant tout ce temps. Le défaut n'était visible que sur une page rendue — d'où sa survie.

Et le commentaire au-dessus affirmait l'inverse : « CSP-safe: attribute, not an inline <style> ». Trois autres composants (ui/progress.tsx, AllocationBar.tsx, Sheet.tsx) portent des commentaires qui disent juste et citent THI-322. C'était le seul endroit à réimplémenter une barre à la main au lieu d'utiliser la primitive partagée.

Les autres composants — vérifiés, pas supposés

Balayage de tous les style={{ du dépôt, puis audit DOM de chaque élément portant un attribut style sur /, /login, /signup, /en :

  • src/app/[locale]/opengraph-image.tsxnext/og (satori), génère un PNG côté serveur. Aucune CSP, non concerné.
  • Tout le reste des attributs style observés en page est posé par JavaScript via le CSSOM (--consent-height, portail des devtools Next, route announcer) — que la CSP n'intercepte pas.
  • La barre des engagements était la seule victime.

Après correctif, les violations restantes sont toutes style-src-elem en provenance de next-devtools — outillage de développement, absent du build de production. Zéro violation imputable à l'application.

Le correctif

CommitmentsClient consomme <Progress>, dont le remplissage est un <rect> SVG à géométrie attributaire — c'est précisément la raison d'être de cette primitive (THI-322).

tone="brand" explicite : l'auto-tone de la primitive vire à warning au-delà de 85 %, ce qui est juste pour un budget qu'on consomme et faux pour une dette qu'on rembourse, où frôler 100 % est la bonne nouvelle.

Progress gagne deux props :

  • ariaLabel — nom accessible sans légende visible (label en affiche une). La ligne nomme déjà l'engagement à l'écran ; la répéter serait du bruit, mais une barre anonyme laisserait un utilisateur d'AT sans savoir à quelle ligne elle appartient.
  • testId — posé sur l'élément role="progressbar", là où les assertions existantes regardent.

Les tests portent sur le mécanisme, pas sur la valeur

Les quatre assertions préexistantes lisaient aria-valuenowelles sont restées vertes pendant tout le défaut. Réaffirmer la valeur n'aurait rien attrapé. Les nouveaux tests assertent donc ce que la politique tue :

  • absence de tout attribut style sous la barre (querySelectorAll('[style]') vide) ;
  • la géométrie du <rect> suit la valeur (12 à 2/17, 0 à 0 payé) ;
  • côté primitive : ariaLabel sans légende visible, priorité sur label, testId sur le bon élément, et « n'exprime jamais sa géométrie par un attribut style ».

Vérification

lint 0 erreur · typecheck 0 · test 140 fichiers / 1847 cas, 100 % · build ✅ · npm run dev démarre et sert (.next purgé entre les deux).

Mesuré dans le navigateur après correctif, trois plans côte à côte :

plan aria-valuenow largeur peinte styles inline
0 sur 11 0 0 % 0
4 sur 11 36 36 % 0
11 sur 11 100 100 % 0

Signalé, non corrigé — avertissement d'hydratation

Présent sur / et /login avant cette PR. La trace le nomme précisément :

<ThemeBootScript>
  <script
+   nonce="NHJUN2h3N1RsbWZYNDdHa002WkI1"
-   nonce=""

Le serveur rend le <script> de bascule de thème avec le nonce, le client le reconstruit avec nonce="" — React efface nonce des propriétés DOM après hydratation, par conception (défense contre l'exfiltration de nonce). Le correctif habituel est un suppressHydrationWarning sur ce <script> précis, ou la lecture du nonce côté client via props.nonce plutôt que l'attribut. Hors périmètre ici : CLAUDE.md exige zéro avertissement console en dev, donc ça mérite sa propre PR plutôt qu'un passager clandestin dans celle-ci.

🤖 Generated with Claude Code

Résumé par Sourcery

Remplacer la barre de progression des engagements par le composant partagé Progress UI afin d’afficher une complétion visuelle fidèle dans le cadre du CSP strict de l’application.

Nouvelles fonctionnalités :

  • Étendre le composant Progress avec les props ariaLabel et testId pour une meilleure accessibilité et des tests plus précis.

Corrections de bugs :

  • Corriger les barres de progression des engagements qui apparaissaient toujours comme étant complètes à 100 % en supprimant les largeurs basées sur les styles inline, bloquées par le CSP, et en utilisant à la place la géométrie SVG.
  • S’assurer que les engagements avec 0 échéances payées affichent un remplissage de progression vide plutôt qu’une barre complète.

Améliorations :

  • Définir explicitement la tonalité de progression des engagements sur la tonalité de la marque afin que la progression du remboursement soit visuellement représentée comme une tendance positive.
  • Renforcer les tests du composant Progress pour couvrir le nom accessible, le placement du test ID et un rendu compatible avec le CSP sans attributs de style inline.
  • Ajouter des tests pour CommitmentsClient qui vérifient que la barre de progression rendue utilise la géométrie SVG sans attributs de style inline et que la largeur suit correctement le ratio payé.
Original summary in English

Summary by Sourcery

Replace the commitments progress bar with the shared Progress UI primitive to render accurate visual completion under the app’s strict CSP.

New Features:

  • Extend the Progress component with ariaLabel and testId props for better accessibility and more precise testing.

Bug Fixes:

  • Fix commitments progress bars always appearing as 100% complete by removing inline style-based widths that were blocked by CSP and using SVG geometry instead.
  • Ensure commitments with 0 paid installments render an empty progress fill rather than a full bar.

Enhancements:

  • Set the commitments progress tone explicitly to the brand tone so repayment progress is visually represented as a positive trend.
  • Strengthen Progress component tests to cover accessible naming, test ID placement, and CSP-safe rendering without inline style attributes.
  • Add CommitmentsClient tests asserting that the rendered progress bar uses SVG geometry with no inline style attributes and that width correctly tracks the paid ratio.

Found while verifying #296. Measured in Chromium under the real CSP, on a
plan at 4 out of 11 instalments: aria-valuenow="36", the fill carried
style="width:36%", and its computed width was 1054px on a 1054px track.
Every plan read as fully repaid.

`style-src` is `self` + nonce (src/proxy.ts:60) — with no `unsafe-inline`
at all in production. A nonce voids `unsafe-inline`, and with no
`style-src-attr` the policy governs style ATTRIBUTES too, so the fill
width was dropped at parse time and the div kept its natural full width.
Screen readers read the right number off aria-valuenow throughout, so the
defect was invisible to anything but a rendered page. In an app about
money, that bar told @Thierry his payment plan was finished.

The comment above it claimed the opposite: "CSP-safe: attribute, not an
inline <style>". Three other components (progress.tsx, AllocationBar,
Sheet) carry comments getting it right and citing THI-322 — this was the
one place that hand-rolled a bar instead of using the shared primitive.

- CommitmentsClient uses `<Progress>`, whose fill is an SVG rect with an
  attribute geometry. Explicit `tone="brand"`: the primitive's auto-tone
  turns warning past 85 %, correct for a budget being consumed and wrong
  for a debt being repaid.
- `Progress` gains `ariaLabel` (accessible name without a visible
  caption) and `testId` (on the progressbar element, where the existing
  assertions look).
- The regression tests assert the ABSENCE of any inline style attribute
  and the rect geometry. The four pre-existing assertions all read
  aria-valuenow and stayed green throughout the defect, so asserting the
  value again would not have caught it.

Measured after the fix, same harness: painted 0 % / 36 % / 100 % against
aria-valuenow 0 / 36 / 100, zero inline styles inside the bars, and zero
application-owned CSP violations left (the remaining ones on any page are
`style-src-elem` from the Next.js dev overlay).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ankora Ready Ready Preview Aug 2, 2026 7:56pm

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sourcery-ai

sourcery-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Guide du relecteur

Réorganise la barre de progression des engagements pour utiliser le composant partagé de type SVG <Progress> au lieu d’une largeur définie via des styles inline, ajoute des props d’accessibilité/de tests à Progress, et renforce les tests afin de garantir un rendu compatible CSP et la géométrie correcte de la barre pour différentes valeurs.

Modifications par fichier

Changement Détails Fichiers
Remplacer la barre de progression des engagements faite maison, qui dépendait d’une largeur en style inline, par le composant partagé <Progress> compatible CSP.
  • Importer le composant partagé Progress dans CommitmentsClient.
  • Supprimer la barre de progression basée sur une div qui utilisait style={{ width: ${progress}% }} et les attributs ARIA/identifiant de test associés.
  • Afficher <Progress> avec value, max, tone="brand", size="sm", ariaLabel et testId sous chaque ligne d’engagement.
  • Mettre à jour le commentaire inline pour documenter le comportement vis‑à‑vis de la CSP et la raison d’utiliser le composant SVG ainsi que un tone explicite.
src/app/[locale]/app/commitments/CommitmentsClient.tsx
Étendre le composant d’interface Progress avec des hooks d’accessibilité et de tests tout en conservant sa géométrie basée sur des attributs SVG et compatible CSP.
  • Ajouter les props ariaLabel et testId à ProgressProps avec des commentaires de documentation sur leur usage prévu.
  • Relier ariaLabel à l’attribut aria-label de l’élément progressbar, en privilégiant ariaLabel par rapport à label et en revenant à un libellé générique en dernier recours.
  • Exposer testId via data-testid sur l’élément progressbar.
  • Préserver la géométrie du rect SVG basée sur value/max, en garantissant qu’aucun attribut de style inline n’est utilisé pour la largeur.
src/components/ui/progress.tsx
Renforcer les tests unitaires pour Progress et CommitmentsClient afin de vérifier le rendu compatible CSP, la géométrie via les attributs SVG, ainsi que les nouveaux comportements d’accessibilité/de tests.
  • Ajouter des tests pour Progress afin de vérifier que ariaLabel définit le nom accessible sans légende visible, que ariaLabel l’emporte sur label pour les lecteurs d’écran, et que testId est placé sur l’élément progressbar.
  • Ajouter un test pour Progress qui vérifie qu’aucun attribut de style inline n’est présent et que la géométrie n’est pas exprimée via style.
  • Ajouter des tests pour CommitmentsClient afin de vérifier que la barre est dessinée sans attributs de style inline, que la largeur du rect SVG suit la valeur numérique, et qu’un plan à 0 payé rend une barre vide plutôt qu’une barre pleine.
  • Conserver les assertions existantes sur aria-valuenow, mais les compléter avec des vérifications centrées sur le mécanisme pour détecter les défauts liés à la CSP.
src/components/ui/__tests__/progress.test.tsx
src/app/[locale]/app/commitments/__tests__/CommitmentsClient.test.tsx

Conseils et commandes

Interagir avec Sourcery

  • Déclencher une nouvelle revue : Commentez @sourcery-ai review sur la pull request.
  • Continuer les discussions : Répondez directement aux commentaires de revue de Sourcery.
  • Générer une issue GitHub à partir d’un commentaire de revue : Demandez à Sourcery de créer une issue à partir d’un commentaire de revue en y répondant. Vous pouvez aussi répondre à un commentaire de revue avec @sourcery-ai issue pour créer une issue à partir de celui‑ci.
  • Générer un titre de pull request : Écrivez @sourcery-ai n’importe où dans le titre de la pull request pour générer un titre à tout moment. Vous pouvez aussi commenter @sourcery-ai title sur la pull request pour (re)générer le titre à tout moment.
  • Générer un résumé de pull request : Écrivez @sourcery-ai summary n’importe où dans le corps de la pull request pour générer un résumé de PR à tout moment exactement à l’endroit souhaité. Vous pouvez aussi commenter @sourcery-ai summary sur la pull request pour (re)générer le résumé à tout moment.
  • Générer le guide du relecteur : Commentez @sourcery-ai guide sur la pull request pour (re)générer le guide du relecteur à tout moment.
  • Résoudre tous les commentaires Sourcery : Commentez @sourcery-ai resolve sur la pull request pour résoudre tous les commentaires de Sourcery. Utile si vous avez déjà traité tous les commentaires et ne souhaitez plus les voir.
  • Ignorer toutes les revues Sourcery : Commentez @sourcery-ai dismiss sur la pull request pour ignorer toutes les revues Sourcery existantes. Particulièrement utile si vous voulez repartir de zéro avec une nouvelle revue — n’oubliez pas de commenter @sourcery-ai review pour déclencher une nouvelle revue !

Personnaliser votre expérience

Accédez à votre tableau de bord pour :

  • Activer ou désactiver des fonctionnalités de revue telles que le résumé de pull request généré par Sourcery, le guide du relecteur, et d’autres.
  • Changer la langue de revue.
  • Ajouter, supprimer ou modifier des instructions de revue personnalisées.
  • Ajuster d’autres paramètres de revue.

Obtenir de l’aide

Original review guide in English

Reviewer's Guide

Refactors the commitments progress bar to use the shared SVG-based Progress primitive instead of a width set via inline styles, adds accessibility/testing props to Progress, and strengthens tests to ensure CSP-safe rendering and correct geometry of the bar under different values.

File-Level Changes

Change Details Files
Replace the hand-rolled commitments progress bar that relied on inline style width with the shared CSP-safe primitive.
  • Import the shared Progress component into CommitmentsClient.
  • Remove the div-based progress bar that used style={{ width: ${progress}% }} and related ARIA attributes/test id.
  • Render with value, max, tone="brand", size="sm", ariaLabel, and testId under each commitment row.
  • Update the inline comment to document the CSP behavior and rationale for using the SVG-based primitive and explicit tone.
src/app/[locale]/app/commitments/CommitmentsClient.tsx
Extend the Progress UI primitive with accessibility and testing hooks while keeping its geometry SVG-attribute-based and CSP-safe.
  • Add ariaLabel and testId props to ProgressProps with documentation comments about their intended usage.
  • Wire ariaLabel into the progressbar element’s aria-label, preferring ariaLabel over label and falling back to a generic label.
  • Expose testId via data-testid on the progressbar element.
  • Preserve SVG rect geometry based on value/max, ensuring no inline style attributes are used for width.
src/components/ui/progress.tsx
Strengthen unit tests for Progress and CommitmentsClient to assert CSP-safe rendering, geometry via SVG attributes, and new accessibility/testing behaviors.
  • Add tests for Progress to verify ariaLabel sets the accessible name without a visible caption, ariaLabel overrides label for screen readers, and testId is placed on the progressbar element.
  • Add a Progress test asserting that no inline style attributes are present and that geometry is not expressed through style.
  • Add CommitmentsClient tests to assert the bar is drawn without inline style attributes, that the SVG rect width tracks the numeric value, and that a 0-paid plan renders an empty bar rather than a full one.
  • Retain existing aria-valuenow assertions, but complement them with mechanism-focused checks to catch CSP-related defects.
src/components/ui/__tests__/progress.test.tsx
src/app/[locale]/app/commitments/__tests__/CommitmentsClient.test.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Salut – j’ai laissé quelques commentaires généraux :

  • Les commentaires en ligne expliquant le comportement de la CSP et le défaut précédent sont assez longs et détaillés ; envisage de les raccourcir et de faire plutôt référence à un ticket ou un document de conception afin de garder la base de code plus facile à parcourir.
  • Le JSDoc pour ProgressProps.label indique toujours qu’il est aussi utilisé comme nom accessible, mais comme ariaLabel a désormais la priorité, cette description devrait être mise à jour pour refléter correctement la logique de nommage.
Invite pour les agents IA
Merci de traiter les commentaires de cette revue de code :

## Commentaires généraux
- Les commentaires en ligne expliquant le comportement de la CSP et le défaut précédent sont assez longs et détaillés ; envisage de les raccourcir et de faire plutôt référence à un ticket ou un document de conception afin de garder la base de code plus facile à parcourir.
- Le JSDoc pour `ProgressProps.label` indique toujours qu’il est aussi utilisé comme nom accessible, mais comme `ariaLabel` a désormais la priorité, cette description devrait être mise à jour pour refléter correctement la logique de nommage.

Sourcery est gratuit pour l’open source – si tu apprécies nos revues, merci d’en parler ✨
Aide-moi à être plus utile ! Clique sur 👍 ou 👎 pour chaque commentaire et j’utiliserai ces retours pour améliorer tes revues.
Original comment in English

Hey - I've left some high level feedback:

  • The inline comments explaining the CSP behavior and prior defect are quite long and detailed; consider shortening them and instead referencing a ticket or design doc to keep the codebase easier to scan.
  • The JSDoc for ProgressProps.label still says it is also used as the accessible name, but with ariaLabel now taking precedence this description should be updated to accurately reflect the naming logic.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The inline comments explaining the CSP behavior and prior defect are quite long and detailed; consider shortening them and instead referencing a ticket or design doc to keep the codebase easier to scan.
- The JSDoc for `ProgressProps.label` still says it is also used as the accessible name, but with `ariaLabel` now taking precedence this description should be updated to accurately reflect the naming logic.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:review-needed Ready for review type:fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant