# Page `/etapes` — « Quelles données pour quelles étapes et quels seuils ? » — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Créer une page statique `/etapes` qui affiche un graphique HTML/CSS montrant quelles données (Approch, Journaux d'annonces légales, BOAMP, JOUE, DECP) sont publiées à chaque étape de la passation d'un marché public et à partir de quel seuil réglementaire. **Architecture:** Une nouvelle page Dash auto-enregistrée (`src/pages/etapes.py`) qui expose un `layout` composé uniquement de `html.Div`/`dcc.Markdown` (aucun callback, aucune donnée dynamique). Le graphique est une grille CSS (1 colonne de libellés + 5 colonnes de seuils) où chaque publication est une barre positionnée en pourcentage. Le style vit dans `src/assets/css/style.css` (auto-chargé par Dash). L'URL est ajoutée au sitemap mais pas à la navbar. **Tech Stack:** Python 3, Dash 3.4 (pages API), CSS (grille + positionnement absolu), Flask (route sitemap existante). --- ## Contexte pour l'engineer (à lire avant de commencer) - decp.info est une app Dash multi-pages. Chaque page est un module dans `src/pages/` qui appelle `register_page(...)` au niveau du module et expose une variable `layout`. Dash découvre ces pages automatiquement grâce à `use_pages=True` (voir `src/app.py:30`). - **Imports** : toujours importer les modules de l'app avec le préfixe `src.` (ex. `from src.utils.seo import META_CONTENT`). - La navbar (`src/app.py:170-182`) est construite à partir d'une **liste blanche de noms** : `["Recherche", "À propos", "Tableau", "Observatoire"]`. Une page dont le `name` n'est pas dans cette liste **n'apparaît pas** dans la navbar. On ne touche donc PAS à la navbar. - Le sitemap (`src/app.py:70-86`) est une **liste d'URLs codée en dur**. Il faut y ajouter `/etapes`. - Le CSS personnalisé est dans `src/assets/css/style.css` (Dash charge automatiquement tout ce qui est dans `src/assets/`). On y ajoute les règles du graphique. - **Pré-requis commit** : ce dépôt utilise `pre-commit` (prettier, ruff). Les hooks ne tournent que si le virtualenv est activé. Avant chaque `git commit`, faire `source .venv/bin/activate` dans la même commande shell. Prettier peut reformater les fichiers Markdown/CSS : si un commit échoue parce que des fichiers ont été modifiés par un hook, refaire `git add` puis `git commit`. - **Référence visuelle** : la maquette validée est `.superpowers/brainstorm/80498-1780599135/content/chart-concept-v3.html`. Le code HTML/CSS ci-dessous en est la transposition. - Ce projet n'a **pas** de test automatisé pour cette page (contenu 100 % statique). La vérification est manuelle via `python run.py`. Les tâches ci-dessous remplacent donc le cycle TDD par des vérifications de rendu explicites. --- ## File Structure - **Create** `src/pages/etapes.py` — la page : `register_page(...)` + `layout`. Contient une petite fonction interne `build_chart()` qui retourne le `html.Div` du graphique, pour garder le `layout` lisible. Responsabilité unique : décrire la page `/etapes`. - **Modify** `src/app.py` — ajouter `"/etapes"` à la liste `pages` de la fonction `sitemap()`. - **Modify** `src/assets/css/style.css` — ajouter un bloc de règles préfixées `.etapes-*` (graphique + responsive). --- ## Task 1 : Squelette de la page `/etapes` **Files:** - Create: `src/pages/etapes.py` - [ ] **Step 1: Créer le fichier avec l'enregistrement de page et un layout minimal** Créer `src/pages/etapes.py` avec exactement ce contenu (le graphique sera ajouté en Task 2) : ```python from dash import dcc, html, register_page from src.utils.seo import META_CONTENT NAME = "Quelles données pour quelles étapes et quels seuils ?" register_page( __name__, path="/etapes", title=f"{NAME} | decp.info", name="Étapes et données", description=( "À chaque étape d'un marché public (programmation, publicité, " "attribution), quelles données sont publiées et à partir de quel " "seuil : DECP, BOAMP, JOUE, journaux d'annonces légales, Approch." ), image_url=META_CONTENT["image_url"], ) layout = html.Div( className="container", children=[ html.H2(NAME), dcc.Markdown( "Un marché public passe par plusieurs étapes. À chacune, des " "données peuvent être publiées — selon le montant du marché et " "des obligations réglementaires. Ce graphique situe les " "principales publications de données par **étape** (de haut en " "bas) et par **seuil** (de gauche à droite, en euros hors taxes)." ), # Le graphique sera inséré ici en Task 2 dcc.Markdown( "**À noter :** l'axe horizontal n'est pas linéaire — les seuils " "sont espacés régulièrement pour rester lisibles. Les étapes " "*Contrat* et *Paiement* n'ont aujourd'hui aucune donnée publiée " "en open data.", className="etapes-note", ), ], ) ``` - [ ] **Step 2: Lancer l'app et vérifier que la page se charge** Run : ```bash source .venv/bin/activate && python run.py ``` Puis ouvrir `http://127.0.0.1:8050/etapes` dans un navigateur. Expected : la page affiche le titre « Quelles données pour quelles étapes et quels seuils ? », le paragraphe d'intro et la note, avec le bandeau de navigation en haut. Aucune erreur dans la console du serveur. Arrêter le serveur (Ctrl-C). - [ ] **Step 3: Vérifier l'absence dans la navbar** Sur n'importe quelle page, vérifier visuellement que « Étapes et données » **n'apparaît pas** dans la barre de navigation (la liste blanche `src/app.py:181` ne la contient pas). Expected : la navbar montre uniquement Recherche / Tableau / Observatoire / À propos. - [ ] **Step 4: Commit** ```bash source .venv/bin/activate && git add src/pages/etapes.py && git commit -m "feat(etapes): squelette de la page /etapes" ``` (Si le commit échoue car un hook a reformaté le fichier : refaire `git add src/pages/etapes.py && git commit -m "feat(etapes): squelette de la page /etapes"`.) --- ## Task 2 : Le graphique HTML/CSS **Files:** - Modify: `src/pages/etapes.py` Le graphique est une grille de 6 colonnes : 1 colonne de libellés d'étape (150 px) + 5 colonnes de seuils égales. L'en-tête X et chaque ligne d'étape occupent les colonnes 2 → 6 (`grid-column: 2 / -1`). À l'intérieur d'une ligne, les barres sont positionnées en `position:absolute` avec `left`/`right` en pourcentage, où chaque segment de seuil = 20 % de la largeur : - Segment 1 (0 € → 40 k€) : 0 % – 20 % - Segment 2 (40 k€ → 90 k€) : 20 % – 40 % - Segment 3 (90 k€ → 140/216 k€) : 40 % – 60 % - Segment 4 (140/216 k€ → 5,404 M€) : 60 % – 80 % - Segment 5 (≥ 5,404 M€) : 80 % – 100 % Une barre qui « commence à 40 k€ et va jusqu'à l'infini » s'écrit donc `left:20%; right:2%` (les `2%` de marge évitent de coller au bord). Une barre qui remplit la case 90 k€ → seuil formalisé s'écrit `left:40%; right:40%`. - [ ] **Step 1: Ajouter la fonction `build_chart()` au-dessus de `layout`** Dans `src/pages/etapes.py`, insérer cette fonction entre le bloc `register_page(...)` et la définition de `layout` : ```python def _lane(*bars): """Une ligne d'étape : fond segmenté en 5 + barres positionnées.""" return html.Div( className="etapes-lane", children=[ html.Div( className="etapes-segs", children=[html.Div() for _ in range(5)], ), *bars, ], ) def _bar(label, color, style): base = {"backgroundColor": color} base.update(style) return html.Div(label, className="etapes-bar", style=base) def build_chart(): return html.Div( className="etapes-chart-scroll", children=html.Div( className="etapes-chart", children=[ # En-tête : coin vide + 5 marqueurs de seuils html.Div(className="etapes-corner"), html.Div( className="etapes-xhead", children=[ html.Div("0 €", className="etapes-xcell"), html.Div( [html.Strong("40 000 €"), "seuil DECP"], className="etapes-xcell", ), html.Div( [html.Strong("90 000 €"), "publicité"], className="etapes-xcell", ), html.Div( [html.Strong("140 k€ / 216 k€"), "seuils formalisés (UE)"], className="etapes-xcell", ), html.Div( [html.Strong("5,404 M€"), "travaux (UE)"], className="etapes-xcell", ), ], ), # Programmation html.Div("Programmation", className="etapes-stage"), _lane( _bar( "Approch — sourcing / préinformation (non réglementaire)", "#7c5cff", {"left": "2%", "right": "2%"}, ), ), # Publicité (appel d'offres) html.Div( ["Publicité ", html.Small("(appel d'offres)")], className="etapes-stage", ), _lane( _bar( "Journaux d'annonces légales", "#f79009", {"left": "40%", "right": "40%", "top": "6px", "height": "20px"}, ), _bar( "BOAMP", "#1570ef", {"left": "40%", "right": "2%", "top": "28px", "height": "20px"}, ), _bar( "JOUE — avis de marché", "#0e9384", {"left": "60%", "right": "2%", "top": "6px", "height": "20px"}, ), ), # Attribution html.Div("Attribution", className="etapes-stage"), _lane( _bar( "DECP — données essentielles", "#12b76a", {"left": "20%", "right": "2%", "top": "6px", "height": "20px"}, ), _bar( "JOUE — avis d'attribution", "#0e9384", {"left": "60%", "right": "2%", "top": "28px", "height": "20px"}, ), ), # Contrat (vide) html.Div("Contrat", className="etapes-stage"), html.Div( "— aucune donnée publiée aujourd'hui —", className="etapes-lane etapes-empty", ), # Paiement (vide) html.Div("Paiement", className="etapes-stage"), html.Div( "— aucune donnée publiée aujourd'hui —", className="etapes-lane etapes-empty", ), ], ), ) def build_legend(): items = [ ("Approch", "#7c5cff"), ("Journaux d'annonces légales", "#f79009"), ("BOAMP", "#1570ef"), ("JOUE", "#0e9384"), ("DECP", "#12b76a"), ] return html.Div( className="etapes-legend", children=[ html.Span( [ html.I(style={"backgroundColor": color}), label, ] ) for label, color in items ], ) ``` - [ ] **Step 2: Insérer le graphique et la légende dans `layout`** Dans `layout`, remplacer la ligne de commentaire `# Le graphique sera inséré ici en Task 2` par : ```python build_chart(), build_legend(), ``` - [ ] **Step 3: Lancer l'app et vérifier le rendu** Run : ```bash source .venv/bin/activate && python run.py ``` Ouvrir `http://127.0.0.1:8050/etapes`. Expected (comparer à la maquette `.superpowers/brainstorm/80498-1780599135/content/chart-concept-v3.html`) : - En-tête X : `0 € · 40 000 € (seuil DECP) · 90 000 € (publicité) · 140 k€/216 k€ (seuils formalisés UE) · 5,404 M€ (travaux UE)`. - Lignes de haut en bas : Programmation (barre Approch pleine largeur), Publicité (Journaux d'annonces légales + BOAMP + JOUE), Attribution (DECP + JOUE), Contrat (vide), Paiement (vide). - La barre « Journaux d'annonces légales » occupe la case 90 k€ → seuil formalisé ; DECP démarre à 40 k€ ; JOUE et BOAMP démarrent aux bons segments. - La légende sous le graphique liste les 5 publications avec leurs couleurs. À ce stade le style brut (couleurs des barres) doit déjà être visible car appliqué inline ; la mise en page de la grille sera finalisée en Task 3. Si la grille n'est pas encore correcte (colonnes non alignées), c'est attendu — continuer en Task 3. Arrêter le serveur. - [ ] **Step 4: Commit** ```bash source .venv/bin/activate && git add src/pages/etapes.py && git commit -m "feat(etapes): graphique données par étape et par seuil" ``` (Si échec dû à un hook : refaire `git add` puis `git commit`.) --- ## Task 3 : CSS du graphique (grille + responsive) **Files:** - Modify: `src/assets/css/style.css` - [ ] **Step 1: Ajouter le bloc CSS à la fin de `src/assets/css/style.css`** Ajouter à la fin du fichier : ```css /* ===== Page /etapes : graphique données par étape et par seuil ===== */ .etapes-chart-scroll { overflow-x: auto; margin: 1rem 0; } .etapes-chart { min-width: 720px; background: #fff; border: 1px solid #d0d5dd; border-radius: 8px; overflow: hidden; font-size: 13px; display: grid; grid-template-columns: 150px repeat(5, 1fr); } .etapes-corner { border-bottom: 2px solid #344054; } .etapes-xhead { grid-column: 2 / -1; display: grid; grid-template-columns: repeat(5, 1fr); border-bottom: 2px solid #344054; } .etapes-xcell { text-align: center; padding: 6px 2px; font-size: 11px; color: #475467; border-left: 1px dashed #d0d5dd; } .etapes-xcell strong { display: block; color: #101828; font-size: 12px; } .etapes-stage { padding: 14px 10px; font-weight: 600; color: #101828; border-bottom: 1px solid #eaecf0; display: flex; align-items: center; } .etapes-stage small { font-weight: 400; color: #667085; } .etapes-lane { grid-column: 2 / -1; position: relative; border-bottom: 1px solid #eaecf0; min-height: 52px; } .etapes-segs { position: absolute; inset: 0; display: grid; grid-template-columns: repeat(5, 1fr); } .etapes-segs > div { border-left: 1px dashed #eaecf0; } .etapes-bar { position: absolute; top: 9px; height: 32px; border-radius: 6px; color: #fff; font-size: 11px; font-weight: 600; display: flex; align-items: center; padding: 0 10px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12); white-space: nowrap; overflow: hidden; } .etapes-empty { color: #98a2b3; font-style: italic; padding: 14px; display: flex; align-items: center; } .etapes-legend { margin-top: 14px; display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; } .etapes-legend span { display: inline-flex; align-items: center; gap: 6px; } .etapes-legend i { width: 14px; height: 14px; border-radius: 3px; display: inline-block; } .etapes-note { margin-top: 8px; color: #667085; font-size: 13px; } ``` - [ ] **Step 2: Lancer l'app et vérifier le rendu final** Run : ```bash source .venv/bin/activate && python run.py ``` Ouvrir `http://127.0.0.1:8050/etapes`. Expected : le graphique est désormais identique à la maquette v3 — colonnes alignées, en-tête X avec ligne de séparation foncée, barres colorées bien positionnées dans chaque segment, lignes Contrat/Paiement grisées en italique, légende sous le graphique. - [ ] **Step 3: Vérifier le responsive (petit écran)** Dans le navigateur, réduire la fenêtre à ~500 px de large (ou ouvrir les devtools en mode mobile). Expected : le graphique devient **défilable horizontalement** (grâce à `.etapes-chart-scroll { overflow-x:auto }` + `min-width:720px`), les barres ne s'écrasent pas. Arrêter le serveur. - [ ] **Step 4: Commit** ```bash source .venv/bin/activate && git add src/assets/css/style.css && git commit -m "feat(etapes): styles du graphique étapes/seuils" ``` (Si échec dû à un hook prettier : refaire `git add` puis `git commit`.) --- ## Task 4 : Référencement de la page dans le sitemap **Files:** - Modify: `src/app.py` (fonction `sitemap()`, ~ligne 73) - [ ] **Step 1: Ajouter `/etapes` à la liste des URLs du sitemap** Dans `src/app.py`, dans la fonction `sitemap()`, modifier la liste `pages` : ```python pages = [ "/", "/observatoire", "/tableau", "/a-propos", "/etapes", ] ``` - [ ] **Step 2: Vérifier le sitemap** Run : ```bash source .venv/bin/activate && python run.py ``` Ouvrir `http://127.0.0.1:8050/sitemap.xml`. Expected : le XML contient désormais une entrée `https://decp.info/etapes`. Arrêter le serveur. - [ ] **Step 3: Commit** ```bash source .venv/bin/activate && git add src/app.py && git commit -m "feat(etapes): référencement de /etapes dans le sitemap" ``` --- ## Vérification finale (checklist de la spec) - [ ] `/etapes` affiche le graphique fidèle à la maquette v3, avec le bandeau de navigation global en haut. - [ ] La page est **absente** de la navbar. - [ ] `/sitemap.xml` **contient** `/etapes`. - [ ] Sur fenêtre étroite, le graphique défile horizontalement sans s'écraser. - [ ] Titre H2 de la page = « Quelles données pour quelles étapes et quels seuils ? ». - [ ] `name` de la page = « Étapes et données ».