# 001 — HistoMétéo MVP — Spécification Technique v2

> **Itération** : v2 — intègre le feedback Reviewer (`feedback-to-architect-001.md`) + deux nouvelles fonctionnalités (icônes météo, résumé journalier).

---

## 0) Contract

- **Source of truth** : ce document (`001-histometeo-mvp.tech.v2.md`)
- **Functional integrity** : aucun critère d'acceptation de `001-histometeo-mvp.md` ne peut être modifié, ignoré ou réinterprété
- **Scope** : fichiers et dossiers autorisés à créer/modifier :
  - `public/` — fichiers frontend (HTML, CSS, JS)
  - `src/` — code backend Python
  - `tests/` — tests (y compris `conftest.py`)
  - Fichiers racine : `requirements.txt`, `.env.example`, `Dockerfile`, `.dockerignore`, `pyproject.toml`
- **Forbidden changes** :
  - `docs/` — aucune modification des specs existantes
  - `.github/` — aucune modification du workflow agent
  - `README.md` — ne sera mis à jour qu'après implémentation complète validée
- **Invariants** :
  - INV-1 : Aucune donnée utilisateur n'est stockée — ni côté serveur, ni côté client
  - INV-2 : Toutes les heures affichées sont en fuseau horaire `Europe/Paris` — aucun offset UTC hardcodé
  - INV-3 : La période maximale par requête est de 31 jours
  - INV-4 : Aucune clé API n'est requise
  - INV-5 : L'interface est intégralement en français **avec accents et diacritiques** (libellés, messages d'erreur, descriptions météo, textes HTML statiques)
  - INV-6 : L'application est une page unique, sans routeur ni navigation multi-pages
  - INV-7 : Aucune injection HTML — tout contenu dynamique est inséré via `textContent` ou création d'éléments DOM, jamais via `innerHTML` avec des données non-échappées
- **Done when** :
  - Les 11 critères d'acceptation (AC1–AC11) sont vérifiables
  - Les 7 corrections bloquantes du feedback (C1–C7) sont résolues
  - Les 4 améliorations recommandées (R1–R4) sont intégrées
  - Les deux nouvelles fonctionnalités (icônes météo, résumé journalier) sont opérationnelles
  - **Tous** les tests passent (aucun test SKIPPED)
  - L'application se lance via `uvicorn src.main:app` ou `docker compose up`

---

## 1) Objectif technique

Livrer l'application HistoMétéo MVP corrigée et enrichie :

1. **Corrections obligatoires** — Résoudre les 7 défauts bloquants identifiés par le Reviewer (accents, timezone, XSS, null handling, configuration pytest)
2. **Icônes météo** — Associer une icône emoji à chaque code WMO pour améliorer la lisibilité du tableau
3. **Résumé journalier** — Calculer et afficher une synthèse météo par jour (temp min/max, cumul pluie, humidité moyenne, vent moyen, condition dominante) au-dessus du tableau horaire

---

## 2) Analyse du brief

### Besoins de correction (feedback C1–C7)

| Correction                           | Source                                                | Sévérité | Nature                             |
| ------------------------------------ | ----------------------------------------------------- | -------- | ---------------------------------- |
| C1 — Configuration pytest-asyncio    | Tests SKIPPED                                         | Bloquant | Fichier `pyproject.toml` manquant  |
| C2 — Offset timezone hardcodé        | `app.js` L97                                          | Bloquant | Bug logique — heures d'été fausses |
| C3 — Faille XSS `innerHTML`          | `app.js` L179-187                                     | Bloquant | Sécurité — injection HTML possible |
| C4 — Accents WMO + typo              | `weather_service.py`                                  | Bloquant | INV-5 violé                        |
| C5 — Accents messages backend        | `weather_service.py`, `commune_service.py`, `main.py` | Bloquant | INV-5 violé                        |
| C6 — Crash `null` valeurs numériques | `weather_service.py` `_normalize_payload`             | Bloquant | TypeError sur données anciennes    |
| C7 — Accents HTML statique           | `index.html`                                          | Bloquant | INV-5 violé                        |

### Améliorations recommandées (R1–R4)

| Amélioration                                                   | Nature                  |
| -------------------------------------------------------------- | ----------------------- |
| R1 — Fermeture `httpx.AsyncClient` via `lifespan`              | Fuite de connexions     |
| R2 — Factoriser `FakeResponse`/`FakeClient` dans `conftest.py` | Duplication test        |
| R3 — Ajouter edge cases tests manquants                        | Couverture insuffisante |
| R4 — Accents messages frontend `app.js`                        | INV-5 complétude        |

### Nouveaux besoins fonctionnels

| Besoin                                          | Complexité | Risque                                                   |
| ----------------------------------------------- | ---------- | -------------------------------------------------------- |
| Icônes météo (mapping WMO → emoji)              | Faible     | Nul — table statique côté backend                        |
| Résumé journalier (agrégation données horaires) | Moyenne    | Faible — calcul côté backend sur données déjà récupérées |

### Contraintes

- Le résumé journalier est calculé **côté backend** à partir des mêmes données horaires déjà récupérées — pas d'appel API supplémentaire.
- Les icônes sont des emoji Unicode (pas d'images, pas de librairie d'icônes).
- Le résumé journalier est affiché **au-dessus** du tableau horaire.

---

## 3) Design minimal proposé

### 3.1 Architecture globale

Inchangée par rapport à la v1. Pas de nouveau service, pas de nouvelle route. Les deux nouvelles fonctionnalités sont des enrichissements de la réponse `GET /api/weather` et de l'affichage frontend.

### 3.2 Modifications par fichier

#### Backend

| Fichier                         | Modification                                                                                                                                                                                  |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pyproject.toml` **(nouveau)**  | Configuration pytest : `asyncio_mode = "auto"` (C1)                                                                                                                                           |
| `src/weather_service.py`        | Rétablir accents WMO (C4), corriger typo "givrant" (C4), accents messages erreur (C5), gestion `null` numériques (C6), ajout champ `icon` dans WMO_DESCRIPTIONS, ajout calcul `daily_summary` |
| `src/commune_service.py`        | Rétablir accents messages erreur (C5)                                                                                                                                                         |
| `src/main.py`                   | Rétablir accents messages erreur (C5), ajouter `lifespan` handler pour fermer les clients httpx (R1), enrichir réponse `/api/weather` avec `daily_summary`                                    |
| `tests/conftest.py`             | Factoriser `FakeResponse`/`FakeClient` (R2)                                                                                                                                                   |
| `tests/test_commune_service.py` | Retirer `FakeResponse`/`FakeClient` dupliqués, utiliser ceux de `conftest.py` (R2), ajouter test caractères accentués (R3)                                                                    |
| `tests/test_weather_service.py` | Retirer `FakeResponse`/`FakeClient` dupliqués (R2), ajouter tests nulls numériques (R3), ajouter test icônes, ajouter test résumé journalier                                                  |
| `tests/test_api.py`             | Ajouter test intégration résumé journalier                                                                                                                                                    |

#### Frontend

| Fichier             | Modification                                                                                                                                                                                        |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `public/index.html` | Rétablir accents textes statiques (C7), ajouter section `#daily-summary` avant le tableau horaire                                                                                                   |
| `public/app.js`     | Corriger `formatDisplayDateTime` sans offset hardcodé (C2), corriger XSS dans `renderRows` (C3), accents messages front (R4), rendu icônes dans colonne Conditions, rendu section résumé journalier |
| `public/style.css`  | Styles pour la section résumé journalier + colonne icônes                                                                                                                                           |

### 3.3 Table WMO enrichie (icônes + descriptions avec accents)

Table de correspondance côté backend (`weather_service.py`). Chaque entrée contient `icon` (emoji) + `description` (texte français avec accents).

| Code WMO | Icône | Description française     |
| -------- | ----- | ------------------------- |
| 0        | ☀️    | Ciel dégagé               |
| 1        | 🌤️    | Principalement dégagé     |
| 2        | ⛅    | Partiellement nuageux     |
| 3        | ☁️    | Couvert                   |
| 45       | 🌫️    | Brouillard                |
| 48       | 🌫️    | Brouillard givrant        |
| 51       | 🌦️    | Bruine légère             |
| 53       | 🌦️    | Bruine modérée            |
| 55       | 🌧️    | Bruine forte              |
| 56       | 🌧️    | Bruine verglaçante légère |
| 57       | 🌧️    | Bruine verglaçante forte  |
| 61       | 🌧️    | Pluie légère              |
| 63       | 🌧️    | Pluie modérée             |
| 65       | 🌧️    | Pluie forte               |
| 66       | 🌧️    | Pluie verglaçante légère  |
| 67       | 🌧️    | Pluie verglaçante forte   |
| 71       | 🌨️    | Chute de neige légère     |
| 73       | 🌨️    | Chute de neige modérée    |
| 75       | 🌨️    | Chute de neige forte      |
| 77       | 🌨️    | Grains de neige           |
| 80       | 🌦️    | Averses légères           |
| 81       | 🌧️    | Averses modérées          |
| 82       | 🌧️    | Averses violentes         |
| 85       | 🌨️    | Averses de neige légères  |
| 86       | 🌨️    | Averses de neige fortes   |
| 95       | ⛈️    | Orage                     |
| 96       | ⛈️    | Orage avec grêle légère   |
| 99       | ⛈️    | Orage avec grêle forte    |

Code non listé → icône `❓`, description `"Conditions non disponibles"`.

### 3.4 Structure de données WMO (backend)

Remplacer le dict `str` par un dict de tuples `(icon, description)` :

```python
WMO_DESCRIPTIONS: dict[int, tuple[str, str]] = {
    0: ("☀️", "Ciel dégagé"),
    1: ("🌤️", "Principalement dégagé"),
    2: ("⛅", "Partiellement nuageux"),
    # ...
}

DEFAULT_WMO = ("❓", "Conditions non disponibles")
```

### 3.5 Réponse API `/api/weather` enrichie

La réponse `GET /api/weather` est enrichie avec :

- Un champ `icon` par entrée horaire
- Un tableau `daily_summary` au même niveau que `data`

**Réponse 200** :

```json
{
  "data": [
    {
      "time": "2024-01-15T00:00",
      "temperature": 2.1,
      "precipitation": 0.0,
      "humidity": 85,
      "wind_speed": 12.5,
      "icon": "☁️",
      "description": "Couvert"
    }
  ],
  "daily_summary": [
    {
      "date": "2024-01-15",
      "temp_min": -1.2,
      "temp_max": 5.8,
      "precipitation_sum": 3.4,
      "humidity_avg": 82,
      "wind_speed_avg": 14.3,
      "icon": "☁️",
      "description": "Couvert"
    }
  ]
}
```

### 3.6 Calcul du résumé journalier (backend)

Le résumé est calculé dans `weather_service.py` après normalisation du payload, à partir des données horaires déjà obtenues. Pas d'appel API supplémentaire.

**Algorithme par jour** :

1. Grouper les entrées horaires par date (extraire `YYYY-MM-DD` du champ `time`)
2. Pour chaque groupe :
   - `temp_min` : `min(temperature)` — ignorer les valeurs `None`
   - `temp_max` : `max(temperature)` — ignorer les valeurs `None`
   - `precipitation_sum` : `sum(precipitation)` — ignorer les valeurs `None`
   - `humidity_avg` : `round(mean(humidity))` — ignorer les valeurs `None`
   - `wind_speed_avg` : `round(mean(wind_speed), 1)` — ignorer les valeurs `None`
   - `icon` + `description` : **condition dominante** = le code WMO le plus fréquent dans la journée (hors `None`). En cas d'égalité, prendre le code WMO le plus élevé (privilégie les conditions les plus significatives : pluie > nuages > dégagé)
3. Si une journée n'a que des valeurs `None` pour un champ, retourner `None` pour ce champ

### 3.7 Gestion des valeurs `null` dans `_normalize_payload` (C6)

Actuellement, seul `weather_code: null` est géré. Les champs numériques (`temperature_2m`, `precipitation`, `relative_humidity_2m`, `wind_speed_10m`) peuvent aussi être `null` dans les données anciennes.

**Règle** : si une valeur est `null`, le champ correspondant dans l'objet de sortie vaut `None` (sérialisé en `null` en JSON). Le frontend doit afficher `"—"` pour ces valeurs.

```python
# Exemple pour temperature
temp_raw = temperatures[idx]
temperature = round(float(temp_raw), 1) if temp_raw is not None else None
```

### 3.8 Correction `formatDisplayDateTime` (C2)

Le bug actuel : `new Date(\`${isoLocalTime}:00+01:00\`)` hardcode l'offset CET (+01:00). En été (CEST), Paris est à +02:00, ce qui décale toutes les heures de 1h.

**Correction** : les timestamps retournés par Open-Meteo avec `timezone=Europe/Paris` sont déjà en heure locale française (ex: `"2024-07-15T14:00"`). Il ne faut **pas** créer un objet `Date` à partir d'un offset UTC. Il suffit de parser la string ISO directement et d'extraire les composants date/heure sans conversion timezone.

**Implémentation** :

```javascript
function formatDisplayDateTime(isoLocalTime) {
  // isoLocalTime = "2024-01-15T14:00" (déjà en heure Europe/Paris)
  const [datePart, timePart] = isoLocalTime.split("T");
  const [year, month, day] = datePart.split("-");
  const [hour, minute] = timePart.split(":");

  // Créer une date UTC fictive pour utiliser Intl.DateTimeFormat
  // afin d'obtenir le jour de la semaine
  const dateObj = new Date(
    Date.UTC(parseInt(year), parseInt(month) - 1, parseInt(day)),
  );
  const weekday = new Intl.DateTimeFormat("fr-FR", {
    weekday: "short",
    timeZone: "UTC",
  }).format(dateObj);

  return `${weekday} ${day}/${month}/${year} ${hour}h${minute}`;
}
```

### 3.9 Correction XSS dans `renderRows` (C3)

**Problème** : `tr.innerHTML = \`<td>${value}</td>\`` injecte du HTML brut. Si l'API renvoyait du contenu malveillant, il serait exécuté.

**Correction** : créer chaque `<td>` via `document.createElement("td")` et assigner le contenu via `textContent`.

```javascript
function renderRows(rows) {
  resultsBody.innerHTML = "";
  rows.forEach((row) => {
    const tr = document.createElement("tr");
    const cells = [
      formatDisplayDateTime(row.time),
      row.temperature !== null ? row.temperature.toFixed(1) : "—",
      row.precipitation !== null ? row.precipitation.toFixed(1) : "—",
      row.humidity !== null ? String(row.humidity) : "—",
      row.wind_speed !== null ? row.wind_speed.toFixed(1) : "—",
      `${row.icon} ${row.description}`,
    ];
    cells.forEach((text) => {
      const td = document.createElement("td");
      td.textContent = text;
      tr.appendChild(td);
    });
    resultsBody.appendChild(tr);
  });
}
```

### 3.10 Section résumé journalier (frontend)

**HTML** : une section `#daily-summary` ajoutée entre `#search` et `#results`, masquée par défaut.

```html
<section id="daily-summary" class="panel hidden" aria-live="polite">
  <h2>Résumé par jour</h2>
  <div class="table-wrapper">
    <table>
      <thead>
        <tr>
          <th>Date</th>
          <th>Temp. min (°C)</th>
          <th>Temp. max (°C)</th>
          <th>Pluie (mm)</th>
          <th>Humidité moy. (%)</th>
          <th>Vent moy. (km/h)</th>
          <th>Conditions</th>
        </tr>
      </thead>
      <tbody id="daily-summary-body"></tbody>
    </table>
  </div>
</section>
```

**JS** : Fonction `renderDailySummary(summaries)` qui peuple le `<tbody>` avec les mêmes protections XSS (création DOM via `textContent`).

**Formatage date du résumé** : `"Mer. 15/01/2024"` (jour de la semaine abrégé + date FR).

### 3.11 API interne — Messages d'erreur corrigés (C5 + R4)

Tous les messages d'erreur doivent inclure les accents français. Liste exhaustive :

#### Backend (`main.py`, `commune_service.py`, `weather_service.py`)

| Message actuel (sans accents)                                       | Message corrigé                                                     |
| ------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `Le parametre de recherche doit contenir au moins 2 caracteres.`    | `Le paramètre de recherche doit contenir au moins 2 caractères.`    |
| `Reessayez dans quelques instants.`                                 | `Réessayez dans quelques instants.`                                 |
| `Le service de donnees meteo est temporairement indisponible.`      | `Le service de données météo est temporairement indisponible.`      |
| `Tous les parametres lat, lon, start et end sont requis.`           | `Tous les paramètres lat, lon, start et end sont requis.`           |
| `La latitude doit etre comprise entre -90 et 90.`                   | `La latitude doit être comprise entre -90 et 90.`                   |
| `La longitude doit etre comprise entre -180 et 180.`                | `La longitude doit être comprise entre -180 et 180.`                |
| `Le format de date doit etre YYYY-MM-DD.`                           | `Le format de date doit être YYYY-MM-DD.`                           |
| `La date de debut ne peut pas etre anterieure au 1er janvier 1940.` | `La date de début ne peut pas être antérieure au 1er janvier 1940.` |
| `La date de fin doit etre posterieure ou egale a la date de debut.` | `La date de fin doit être postérieure ou égale à la date de début.` |
| `La periode est limitee a 31 jours maximum.`                        | `La période est limitée à 31 jours maximum.`                        |
| `Seules les dates passees sont disponibles.`                        | `Seules les dates passées sont disponibles.`                        |

#### Frontend (`app.js`)

| Message actuel (sans accents)                                       | Message corrigé                                                     |
| ------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `Saisissez au moins 2 caracteres.`                                  | `Saisissez au moins 2 caractères.`                                  |
| `Aucune commune trouvee pour cette recherche.`                      | `Aucune commune trouvée pour cette recherche.`                      |
| `La date de debut ne peut pas etre anterieure au 1er janvier 1940.` | `La date de début ne peut pas être antérieure au 1er janvier 1940.` |
| `Seules les dates passees sont disponibles.`                        | `Seules les dates passées sont disponibles.`                        |
| `La date de fin doit etre posterieure ou egale a la date de debut.` | `La date de fin doit être postérieure ou égale à la date de début.` |
| `La periode est limitee a 31 jours maximum.`                        | `La période est limitée à 31 jours maximum.`                        |

#### HTML (`index.html`)

Tous les textes statiques doivent porter les accents :

- `Retrouvez la météo passée, heure par heure…`
- `Période disponible : du 01/01/1940 à hier.`
- `Résultats`
- `Température (°C)` / `Précipitations (mm)` / `Humidité (%)` etc.
- `Transparence des données`
- `Les données présentées proviennent d'une reconstitution météorologique par modèle numérique…`
- `Données : geo.api.gouv.fr et Open-Meteo Historical Archive API.`

### 3.12 Fermeture httpx.AsyncClient via lifespan (R1)

Utiliser le `lifespan` context manager de FastAPI pour instancier et fermer proprement les clients HTTP.

```python
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup : les services sont déjà instanciés au module level
    yield
    # Shutdown : fermer les clients HTTP
    await commune_service.client.aclose()
    await weather_service.client.aclose()

app = FastAPI(title="HistoMétéo", lifespan=lifespan)
```

---

## 4) Plan d'implémentation

### Étape 1 — Configuration et corrections infrastructure (C1, R1)

**Fichiers** : `pyproject.toml` (nouveau), `src/main.py`

- Créer `pyproject.toml` avec `[tool.pytest.ini_options] asyncio_mode = "auto"`
- Ajouter le `lifespan` handler dans `main.py` pour fermer les clients httpx au shutdown

**Testable** : `pytest` exécute tous les tests (aucun SKIPPED). Vérifier que les 7 tests précédemment skippés passent maintenant.

### Étape 2 — Corrections backend : accents, typo, nulls (C4, C5, C6)

**Fichiers** : `src/weather_service.py`, `src/commune_service.py`, `src/main.py`

- Rétablir accents dans `WMO_DESCRIPTIONS`, corriger "givrante" → "givrant"
- Enrichir `WMO_DESCRIPTIONS` : transformer en dict de tuples `(icon, description)`
- Ajouter le `DEFAULT_WMO` pour les codes non listés
- Rétablir accents dans tous les messages d'erreur backend
- Gérer les `null` pour `temperature_2m`, `precipitation`, `relative_humidity_2m`, `wind_speed_10m` dans `_normalize_payload` → `None` si la valeur source est `null`
- Enrichir chaque entrée horaire avec le champ `icon`

**Testable** : Tests unitaires `test_weather_service.py` et `test_commune_service.py` passent, y compris les tests de nulls numériques.

### Étape 3 — Résumé journalier backend

**Fichiers** : `src/weather_service.py`

- Ajouter une méthode `_compute_daily_summary(hourly_data: list[dict]) -> list[dict]` dans `WeatherService`
- Grouper par date, calculer min/max/sum/avg, déterminer condition dominante
- Modifier `get_weather` pour retourner `{"data": [...], "daily_summary": [...]}`
- Adapter le cache si nécessaire (la structure de retour change : retourner un dict et non une liste)

**Testable** : Test unitaire de `_compute_daily_summary` avec données multi-jours, valeurs nulles, journée à un seul code WMO.

### Étape 4 — Corrections frontend (C2, C3, C7, R4)

**Fichiers** : `public/index.html`, `public/app.js`

- Rétablir tous les accents dans `index.html` (textes statiques)
- Corriger `formatDisplayDateTime` — parser la string ISO sans offset UTC (C2)
- Corriger `renderRows` — remplacer `innerHTML` par création d'éléments DOM avec `textContent` (C3)
- Rétablir accents dans les messages d'erreur et messages utilisateur de `app.js` (R4)
- Adapter `renderRows` pour afficher `icon + description` dans la colonne Conditions
- Gérer `null` côté affichage → `"—"` pour les valeurs numériques absentes

**Testable** : Flow complet fonctionne dans le navigateur. Heures d'été affichées correctement. Aucune injection HTML possible.

### Étape 5 — Résumé journalier frontend

**Fichiers** : `public/index.html`, `public/app.js`, `public/style.css`

- Ajouter la section HTML `#daily-summary` entre `#search` et `#results`
- Implémenter `renderDailySummary(summaries)` dans `app.js` — création DOM sécurisée (pas d'innerHTML)
- Adapter le handler du bouton Rechercher pour afficher le résumé avant le tableau horaire
- Ajouter les styles CSS pour la section résumé (cohérence avec le design existant)

**Testable** : Après une recherche valide, la section résumé s'affiche au-dessus du tableau horaire avec les valeurs agrégées correctes.

### Étape 6 — Tests (R2, R3 + nouveaux tests)

**Fichiers** : `tests/conftest.py`, `tests/test_commune_service.py`, `tests/test_weather_service.py`, `tests/test_api.py`

- Factoriser `FakeResponse`/`FakeClient` dans `conftest.py` (R2)
- Ajouter edge cases manquants (R3) : caractères accentués, coordonnées hors range, nulls numériques
- Ajouter tests pour `icon` dans la réponse normalisée
- Ajouter tests pour `_compute_daily_summary` (multi-jours, nulls, condition dominante, égalité codes WMO)
- Ajouter test intégration : `GET /api/weather` retourne `daily_summary` en plus de `data`

**Testable** : `pytest -q` — tous les tests passent, aucun SKIPPED.

---

## 5) Guide pour le Développeur

### Pièges fréquents

- **Coordonnées GeoJSON inversées** : `geo.api.gouv.fr` retourne `centre.coordinates` en `[longitude, latitude]`. Le backend inverse avant de renvoyer au front. Ne pas toucher à cette logique.
- **Fuseau horaire — NE PAS créer de `Date` avec offset** : les timestamps Open-Meteo avec `timezone=Europe/Paris` sont déjà en heure locale. Parser la string directement (`split("T")`) pour extraire date et heure. Ne jamais construire `new Date(\`...:00+01:00\`)`ni`new Date(\`...:00+02:00\`)`.
- **Valeurs `null` dans les données** : Open-Meteo peut renvoyer `null` pour **n'importe quel** champ numérique (`temperature_2m`, `precipitation`, etc.) sur les données très anciennes. Tester `is not None` avant tout `float()` ou `int()`.
- **Debounce** : ne pas oublier d'annuler le timer précédent à chaque frappe.
- **Emoji et encodage** : les fichiers Python doivent être en UTF-8. Les emoji dans `WMO_DESCRIPTIONS` seront sérialisés correctement en JSON. Côté HTML, le `<meta charset="UTF-8">` est déjà en place.
- **Structure de retour `get_weather`** : en v2, le retour est un **dict** `{"data": [...], "daily_summary": [...]}` et non plus une **liste**. Le cache stocke désormais ce dict. Adapter le type hint et les tests.

### Zones de dérive à éviter

- **Ne pas ajouter d'images/sprites** pour les icônes météo — emoji Unicode uniquement.
- **Ne pas calculer le résumé côté frontend** — c'est une responsabilité backend.
- **Ne pas ajouter de persistance** (fichier, SQLite, localStorage).
- **Ne pas implémenter de graphiques** — hors scope.
- **Ne pas créer de système de composants** côté front.

### Simplifications autorisées

- Le résumé journalier est calculé à chaque requête (pas de cache séparé) — les données horaires sont déjà cachées, le calcul d'agrégation est négligeable.
- La condition dominante par jour est déterminée par comptage simple (mode) — pas besoin d'un algorithme sophistiqué.
- Le `lifespan` handler ferme les clients au shutdown mais ne gère pas les reconnexions — acceptable pour le MVP.

### Décisions explicitement interdites

- **Interdiction d'introduire un framework JS** (React, Vue…).
- **Interdiction d'ajouter une base de données**.
- **Interdiction d'utiliser `innerHTML`** avec des données dynamiques.
- **Interdiction de hardcoder un offset UTC** pour formater les heures.
- **Interdiction d'appeler une API supplémentaire** pour le résumé journalier (les données Open-Meteo daily ne sont pas utilisées — tout est calculé à partir des données horaires existantes).

---

## 6) Stratégie de tests

### Configuration pytest

Fichier `pyproject.toml` à la racine :

```toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
```

### Tests unitaires

| Fichier                   | Cible                | Cas testés                                                                                                                                                                                                                                                                                                       |
| ------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `test_cache.py`           | `cache.py`           | Insertion/lecture, expiration TTL, eviction FIFO à taille max, clé inexistante retourne `None`                                                                                                                                                                                                                   |
| `test_commune_service.py` | `commune_service.py` | Transformation coordonnées GeoJSON → lat/lon, réponse vide, erreur upstream, **caractères accentués dans la recherche** (`"saint-étienne"`)                                                                                                                                                                      |
| `test_weather_service.py` | `weather_service.py` | Normalisation payload + WMO avec accents et icônes, `weather_code: null` → icône/description par défaut, erreur upstream, longueurs incohérentes, **nulls numériques** (`temperature_2m: null` → `None`), **résumé journalier** (multi-jours, condition dominante, égalité WMO, toutes valeurs null sur un jour) |

### Tests d'intégration

| Fichier       | Cible          | Cas testés                                                                                 |
| ------------- | -------------- | ------------------------------------------------------------------------------------------ |
| `test_api.py` | Endpoints HTTP | `GET /api/communes?q=par` → 200 + structure correcte                                       |
|               |                | `GET /api/communes?q=a` → 400 (trop court)                                                 |
|               |                | `GET /api/communes` (sans q) → 400                                                         |
|               |                | `GET /api/weather` paramètres valides → 200 + `data` avec `icon` + `daily_summary` présent |
|               |                | `GET /api/weather` période > 31 jours → 400                                                |
|               |                | `GET /api/weather` date future → 400                                                       |
|               |                | `GET /api/weather` date < 1940-01-01 → 400                                                 |
|               |                | `GET /api/weather` date fin < date début → 400                                             |

### Fixtures partagées (`conftest.py`)

```python
class FakeResponse:
    def __init__(self, json_data, status_code=200):
        self.json_data = json_data
        self.status_code = status_code

    def json(self):
        return self.json_data

    def raise_for_status(self):
        if self.status_code >= 400:
            raise httpx.HTTPStatusError(
                "error", request=None, response=self
            )

class FakeClient:
    def __init__(self, response: FakeResponse):
        self.response = response

    async def get(self, url, params=None):
        return self.response

    async def aclose(self):
        pass
```

### Edge cases critiques

- Requête commune avec caractères accentués (`"saint-étienne"`, `"île-de-france"`)
- `weather_code: null` → icône `❓` + description `"Conditions non disponibles"`
- `temperature_2m: null` pour une heure → `None` dans la réponse + `"—"` à l'affichage
- Résumé journalier avec toutes les températures `null` sur un jour → `temp_min: null`, `temp_max: null`
- Résumé journalier avec un seul code WMO sur 24h → ce code est la condition dominante
- Résumé journalier avec égalité de codes WMO → le code le plus élevé l'emporte
- Période d'exactement 31 jours (limite inclusive)

---

## 7) Risques techniques

| #   | Risque                                                                      | Probabilité             | Impact                                                            | Mitigation                                                                                                           |
| --- | --------------------------------------------------------------------------- | ----------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| 1   | **Emoji non rendus sur certains OS/navigateurs**                            | Faible                  | Faible — dégradation gracieuse, le texte descriptif reste lisible | Les emoji WMO sont tous dans le jeu Unicode standard. Si non rendus, l'utilisateur voit le texte descriptif à côté.  |
| 2   | **Changement de structure de retour `get_weather`** casse le cache existant | Certaine au déploiement | Faible — cache en mémoire perdu au redémarrage                    | Le cache est in-memory et perdu au redémarrage. Aucune migration nécessaire. Documenter dans le plan de déploiement. |
| 3   | **Performance du calcul résumé** pour 31 jours × 24h = 744 lignes           | Nulle                   | Nul — agrégation triviale en Python                               | Boucle simple sur un list de dicts. Temps négligeable (<1ms).                                                        |

---

## Annexe A — Traçabilité du feedback

| Correction feedback                     | Traitement dans cette spec          |
| --------------------------------------- | ----------------------------------- |
| C1 — `pyproject.toml` asyncio_mode      | §4 Étape 1, §6 Configuration pytest |
| C2 — Offset timezone hardcodé           | §3.8                                |
| C3 — Faille XSS `innerHTML`             | §3.9, INV-7                         |
| C4 — Accents WMO + typo                 | §3.3, §3.4                          |
| C5 — Accents messages backend           | §3.11                               |
| C6 — Crash `null` valeurs numériques    | §3.7                                |
| C7 — Accents HTML statique              | §3.11 (section HTML)                |
| R1 — Fermeture httpx clients            | §3.12                               |
| R2 — Factoriser FakeResponse/FakeClient | §6 Fixtures partagées               |
| R3 — Edge cases manquants               | §6 Edge cases critiques             |
| R4 — Accents messages frontend          | §3.11 (section Frontend)            |
