Plan: page /etapes — données par étape et par seuil
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,515 @@
|
|||||||
|
# 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 `<loc>https://decp.info/etapes</loc>`. 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 ».
|
||||||
Reference in New Issue
Block a user