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

> **Itération** : v4 — intègre le feedback Reviewer (`feedback-to-architect-001-v3.md`) + deux nouvelles fonctionnalités (périodes prédéfinies, mode comparaison deux villes).

---

## 0) Contract

- **Source of truth** : ce document (`001-histometeo-mvp.tech.v4.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`, `Dockerfile`, `.dockerignore`, `pyproject.toml`, `README.md`
- **Forbidden changes** :
  - `docs/` — aucune modification des specs existantes
  - `.github/` — aucune modification du workflow agent
- **Invariants** (héritent intégralement de v3, renumérotés) :
  - INV-1 : Aucune donnée utilisateur n'est stockée — ni côté serveur, ni côté client (pas de base de données, pas de `localStorage`, pas de fichier)
  - 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** — y compris le nom de l'application (`HistoMétéo`)
  - INV-6 : L'application est une page unique — pas de routeur multi-pages, l'URL reflète l'état de recherche via query parameters
  - INV-7 : Aucune injection HTML — tout contenu dynamique est inséré via `textContent`, `createElement` ou `replaceChildren()`, jamais via `innerHTML` avec des données non-échappées
  - INV-8 _(nouveau)_ : Le flux principal (recherche simple une commune) reste pleinement fonctionnel en l'absence d'activation du mode comparaison — aucune régression UX
- **Done when** :
  - Les 11 critères d'acceptation originaux (AC1–AC11) restent vérifiables
  - Les améliorations des feedbacks v2 (R1–R5) et v3 (R6–R9) sont intégrées
  - Les fonctionnalités v3 restent opérationnelles (graphique, URL partageable, regroupement par jour)
  - Les 2 nouvelles fonctionnalités sont opérationnelles : périodes prédéfinies, mode comparaison
  - **Tous** les tests passent (aucun test SKIPPED, aucun DeprecationWarning projet)
  - L'application se lance via `uvicorn src.main:app` ou `docker compose up`

---

## 1) Objectif technique

Enrichir l'application HistoMétéo MVP avec deux fonctionnalités et intégrer les améliorations mineures du feedback v3 :

1. **Périodes prédéfinies** — Boutons de raccourci (hier, 3j, 7j, 15j, 30j) pour sélectionner rapidement une plage de dates sans manipulation manuelle des champs date
2. **Mode comparaison entre deux villes** — Interface secondaire permettant de comparer les données météo de deux communes sur une même période, avec résumé comparatif et affichage côte à côte
3. **Corrections mineures feedback v3** — `replaceChildren()` au lieu de `innerHTML = ""`, ajout département dans URL partageable

---

## 2) Analyse du brief

### Nouveaux besoins fonctionnels

| Besoin                    | Complexité | Risque                                                                      |
| ------------------------- | ---------- | --------------------------------------------------------------------------- |
| Périodes prédéfinies      | Faible     | Aucun — logique de dates déjà en place, ajout de boutons déclencheurs       |
| Mode comparaison 2 villes | Élevée     | Moyen — double appel API weather, nouveau layout comparatif, gestion d'état |

### Améliorations feedback v3 (R6–R9)

| #   | Amélioration                                                                                        | Nature                    |
| --- | --------------------------------------------------------------------------------------------------- | ------------------------- |
| R6  | Remplacer `innerHTML = ""` par `replaceChildren()` dans `showSuggestions` et `renderDailySummary`   | Sécurité défensive        |
| R7  | Ajouter le département dans l'URL (`dept` param) pour afficher `Nom (dept)` au chargement par URL   | UX Polish                 |
| R8  | Surveiller mise à jour FastAPI pour les DeprecationWarnings (aucune action code, attente upstream)  | Info — rien à implémenter |
| R9  | Dockerfile → `python:3.14-slim` quand stabilisé (non bloquant, non implémenté dans cette itération) | Info différée             |

> R8 et R9 ne génèrent aucune modification de code dans cette itération.

### Contraintes

- **Pas de framework JS ajouté** — le projet reste en vanilla JS. Pas de React, pas de composants.
- **Pas de route backend supplémentaire** — l'endpoint `/api/weather` existant est appelé deux fois (une par ville) côté frontend en mode comparaison. Pas de nouvel endpoint `/api/compare`.
- **Pas de nouvelle dépendance Python** — aucune librairie backend ajoutée.
- **Le mode comparaison n'est pas le flux par défaut** — Il s'active explicitement après ou pendant une recherche simple. Le formulaire principal reste inchangé.
- **Chart.js est réutilisé** tel quel pour le graphique comparatif (datasets multi-villes sur le même graphique).

---

## 3) Design minimal proposé

### 3.1 Architecture globale

Le backend reste **strictement inchangé** : mêmes routes, mêmes services, même structure de réponse API. Le frontend assure la double orchestration des appels et le rendu comparatif.

### 3.2 Vue d'ensemble des modifications

```
Backend : AUCUNE modification fonctionnelle

Frontend (modifications)
├── public/index.html        → Boutons périodes prédéfinies, section comparaison (2e commune + résumé comparatif)
├── public/app.js            → Logique périodes, état comparaison, double fetch, rendu comparatif
├── public/style.css         → Styles boutons raccourcis, layout comparatif
```

### 3.3 Fonctionnalité 1 — Périodes prédéfinies

#### Principe

Un groupe de boutons cliquables est affiché sous les champs de dates. Chaque bouton remplit automatiquement les deux champs date (`date-start`, `date-end`) avec la période correspondante, calculée en fonction de la date courante (la veille étant la date la plus récente autorisée).

#### Périodes

| Label bouton      | `date-start`      | `date-end` |
| ----------------- | ----------------- | ---------- |
| Hier              | veille            | veille     |
| 3 derniers jours  | veille − 2 jours  | veille     |
| 7 derniers jours  | veille − 6 jours  | veille     |
| 15 derniers jours | veille − 14 jours | veille     |
| 30 derniers jours | veille − 29 jours | veille     |

> Toutes les périodes respectent INV-3 (≤ 31 jours).

#### Comportement

- Les boutons sont **désactivés** tant qu'aucune commune n'est sélectionnée (même état que les champs date).
- Un clic sur un bouton :
  1. Met à jour `dateStart.value` et `dateEnd.value`
  2. Déclenche `isDateRangeValid()` pour mise à jour du message date
  3. Appelle `updateSearchButtonState()` pour activer le bouton Rechercher
  4. Ajoute une classe CSS `.active` sur le bouton cliqué (feedback visuel) ; les autres boutons perdent la classe `.active`
- Si l'utilisateur modifie manuellement un champ date après un clic raccourci, la classe `.active` est retirée de tous les boutons raccourci.
- Les boutons sont positionnés entre les champs date et le bouton Rechercher.

#### HTML

Ajout d'un `<div class="preset-buttons">` contenant 5 `<button type="button">` avec attribut `data-days` (0, 3, 7, 15, 30) :

```html
<div class="preset-buttons" id="preset-buttons">
  <button type="button" data-days="0" disabled>Hier</button>
  <button type="button" data-days="3" disabled>3 jours</button>
  <button type="button" data-days="7" disabled>7 jours</button>
  <button type="button" data-days="15" disabled>15 jours</button>
  <button type="button" data-days="30" disabled>30 jours</button>
</div>
```

> `data-days="0"` → hier seul. `data-days="N"` → les N-1 jours avant la veille + la veille.

#### JS — Fonction `applyPresetPeriod(days)`

```
function applyPresetPeriod(days) {
  const end = new Date(maxDateValue);          // veille
  const start = new Date(maxDateValue);
  start.setDate(end.getDate() - Math.max(0, days - 1));  // days=0 → même jour
  dateStart.value = start.toISOString().slice(0, 10);
  dateEnd.value = end.toISOString().slice(0, 10);
  isDateRangeValid();
  updateSearchButtonState();
}
```

#### CSS

```css
.preset-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.6rem 0;
}

.preset-buttons button {
  margin-top: 0;
  padding: 0.35rem 0.7rem;
  font-size: 0.85rem;
  border-radius: 8px;
  background: transparent;
  border: 1px solid var(--accent);
  color: var(--accent);
  cursor: pointer;
}

.preset-buttons button[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
}

.preset-buttons button.active {
  background: var(--accent);
  color: #fff;
}
```

### 3.4 Fonctionnalité 2 — Mode comparaison entre deux villes

#### 3.4.1 Principe d'UX

Le mode comparaison est **secondaire** et non intrusif :

- Un bouton « Comparer avec une autre ville » apparaît sous le formulaire de recherche une fois qu'une commune est sélectionnée.
- Activer le mode comparaison affiche un **second champ commune** (avec la même auto-complétion que le premier).
- La **période est partagée** : un seul couple de dates pour les deux villes.
- Un bouton « Annuler la comparaison » permet de revenir au mode simple (masque le second champ et les résultats comparatifs).

#### 3.4.2 État applicatif

Variables supplémentaires dans `app.js` :

```js
let comparisonMode = false; // toggle global
let selectedCommune2 = null; // 2e commune sélectionnée
```

Le `selectedCommune` existant reste la ville principale. `selectedCommune2` suit le même schéma `{ nom, departement, latitude, longitude }`.

#### 3.4.3 Flux utilisateur mode comparaison

1. L'utilisateur sélectionne une première commune → les dates s'activent
2. Il clique sur « Comparer avec une autre ville »
3. Le second champ commune apparaît (avec auto-complétion identique)
4. Il sélectionne la seconde commune
5. Il choisit les dates (ou utilise un raccourci période)
6. Il clique « Rechercher »
7. Le frontend lance **deux appels parallèles** à `/api/weather` avec les mêmes dates mais les coordonnées respectives des deux communes
8. Les résultats s'affichent en mode comparatif

#### 3.4.4 Double fetch et gestion d'erreur

```js
async function fetchWeatherForCommune(commune) {
  const params = new URLSearchParams({
    lat: commune.latitude,
    lon: commune.longitude,
    start: dateStart.value,
    end: dateEnd.value,
  });
  const response = await fetch(`/api/weather?${params.toString()}`);
  const payload = await response.json();
  if (!response.ok) throw new Error(payload.error || "Erreur inconnue.");
  return payload;
}
```

Dans `performSearch()` :

```js
if (comparisonMode && selectedCommune2) {
  const [result1, result2] = await Promise.all([
    fetchWeatherForCommune(selectedCommune),
    fetchWeatherForCommune(selectedCommune2),
  ]);
  renderComparisonResults(result1, result2);
} else {
  const payload = await fetchWeatherForCommune(selectedCommune);
  // rendu simple existant
}
```

Si une des deux requêtes échoue, le message d'erreur global est affiché. Pas de rendu partiel.

#### 3.4.5 URL partageable en mode comparaison

Extension des query parameters existants :

| Paramètre  | Description                | Mode simple | Mode comparaison |
| ---------- | -------------------------- | ----------- | ---------------- |
| `commune`  | Nom commune 1              | ✅          | ✅               |
| `dept`     | Département commune 1 (R7) | ✅          | ✅               |
| `lat`      | Latitude commune 1         | ✅          | ✅               |
| `lon`      | Longitude commune 1        | ✅          | ✅               |
| `start`    | Date de début              | ✅          | ✅               |
| `end`      | Date de fin                | ✅          | ✅               |
| `commune2` | Nom commune 2              | —           | ✅               |
| `dept2`    | Département commune 2      | —           | ✅               |
| `lat2`     | Latitude commune 2         | —           | ✅               |
| `lon2`     | Longitude commune 2        | —           | ✅               |

Au chargement, si `commune2` / `lat2` / `lon2` sont présents et valides, le mode comparaison est activé automatiquement.

#### 3.4.6 Rendu comparatif

##### a) Résumé comparatif (nouvelle section `#comparison-summary`)

Tableau ou cartes affichant les métriques clés côte à côte :

| Indicateur             | Ville 1   | Ville 2  | Écart / Note            |
| ---------------------- | --------- | -------- | ----------------------- |
| Température min        | X °C      | Y °C     | —                       |
| Température max        | X °C      | Y °C     | ville la plus chaude    |
| Temp. moyenne          | X °C      | Y °C     | écart thermique moyen   |
| Précipitations totales | X mm      | Y mm     | ville la plus pluvieuse |
| Vent moyen             | X km/h    | Y km/h   | —                       |
| Condition dominante    | ☀️ Dégagé | 🌧️ Pluie | —                       |

Les valeurs agrégées sont calculées **côté frontend** à partir des `daily_summary` retournés par l'API.

Fonctions utilitaires frontend :

```js
function computeAggregates(dailySummaries) {
  // Retourne { tempMin, tempMax, tempAvg, precipTotal, windAvg, dominantIcon, dominantDesc }
  // depuis la liste de daily_summary
}
```

Le label « ville la plus chaude / pluvieuse » est un simple texte conditionnel, affiché uniquement si l'écart est > 0.

##### b) Graphique comparatif

Le graphique Chart.js existant est étendu avec des datasets supplémentaires :

- **4 datasets** au lieu de 2 :
  - Température ville 1 (ligne orange — existant)
  - Précipitations ville 1 (barres bleues — existant)
  - Température ville 2 (ligne rouge, `borderDash: [5, 5]` pour différencier)
  - Précipitations ville 2 (barres cyan, opacité réduite)
- Les labels des datasets incluent le nom de la ville : ex. `Température — Paris (°C)` / `Température — Lyon (°C)`
- La légende Chart.js distingue clairement les deux villes

La fonction `renderChart` accepte un paramètre optionnel pour les données de la seconde ville :

```js
function renderChart(
  hourlyData1,
  hourlyData2 = null,
  communeName1 = "",
  communeName2 = "",
) {
  // Si hourlyData2 est null → rendu simple actuel
  // Sinon → 4 datasets
}
```

##### c) Résumés journaliers côte à côte

Le tableau de résumé journalier existant (`#daily-summary`) est étendu en mode comparaison :

| Date | V1 Temp min | V1 Temp max | V1 Pluie | V2 Temp min | V2 Temp max | V2 Pluie | Conditions V1 | Conditions V2 |
| ---- | ----------- | ----------- | -------- | ----------- | ----------- | -------- | ------------- | ------------- |

Alternative plus lisible : **deux tableaux l'un sous l'autre**, chacun préfixé par le nom de la ville dans un `<h3>`.

> **Choix retenu** : deux tableaux l'un sous l'autre. Plus simple, plus lisible en mobile, réutilise `renderDailySummary` tel quel. Un wrapper affiche un titre `<h3>` avant chaque bloc.

##### d) Détail horaire (accordéon)

En mode comparaison, **deux blocs d'accordéon** sont rendus l'un sous l'autre, chacun précédé d'un `<h3>` avec le nom de la ville. La fonction `renderDayGroups` est réutilisée telle quelle pour chaque ville.

#### 3.4.7 HTML — Modifications

Ajout dans la section `#search` :

```html
<div id="compare-toggle-area">
  <button type="button" id="compare-button" class="btn-secondary" disabled>
    Comparer avec une autre ville
  </button>
</div>

<div id="compare-fields" class="hidden">
  <label for="commune2-input">Seconde commune</label>
  <div class="autocomplete">
    <input
      id="commune2-input"
      type="text"
      autocomplete="off"
      placeholder="Ex : Lyon"
      aria-autocomplete="list"
      aria-expanded="false"
      aria-controls="commune2-list"
    />
    <ul id="commune2-list" class="suggestions hidden" role="listbox"></ul>
  </div>
  <p id="commune2-message" class="field-message" aria-live="polite"></p>
  <button type="button" id="cancel-compare" class="btn-cancel">
    Annuler la comparaison
  </button>
</div>
```

Ajout d'une section résumé comparatif :

```html
<section id="comparison-summary" class="panel hidden" aria-live="polite">
  <h2>Comparaison</h2>
  <div id="comparison-summary-body"></div>
</section>
```

#### 3.4.8 CSS — Mode comparaison

```css
.btn-cancel {
  margin-top: 0.5rem;
  background: transparent;
  border: 1px solid var(--error);
  color: var(--error);
  padding: 0.4rem 0.9rem;
  border-radius: 8px;
  cursor: pointer;
  font-size: 0.9rem;
}

.btn-cancel:hover {
  background: var(--error);
  color: #fff;
}

.comparison-table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 1rem;
}

.comparison-table th,
.comparison-table td {
  text-align: center;
  padding: 0.55rem;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.comparison-table th {
  background: #f3efe6;
}

.comparison-winner {
  font-weight: 600;
  color: var(--accent);
}

.comparison-city-title {
  font-size: 1.1rem;
  font-weight: 600;
  margin: 1rem 0 0.5rem;
  color: var(--text);
}
```

### 3.5 Amélioration R6 — `replaceChildren()` au lieu de `innerHTML = ""`

Remplacer les deux occurrences dans `app.js` :

| Avant                             | Après                                |
| --------------------------------- | ------------------------------------ |
| `communeList.innerHTML = ""`      | `communeList.replaceChildren()`      |
| `dailySummaryBody.innerHTML = ""` | `dailySummaryBody.replaceChildren()` |

### 3.6 Amélioration R7 — Département dans l'URL

Modifier `updateURL` pour inclure `dept` :

```js
function updateURL(
  commune,
  dept,
  lat,
  lon,
  start,
  end,
  commune2,
  dept2,
  lat2,
  lon2,
) {
  const params = new URLSearchParams({
    commune,
    dept,
    lat: String(lat),
    lon: String(lon),
    start,
    end,
  });
  if (commune2 && lat2 !== undefined && lon2 !== undefined) {
    params.set("commune2", commune2);
    params.set("dept2", dept2);
    params.set("lat2", String(lat2));
    params.set("lon2", String(lon2));
  }
  history.replaceState(null, "", `?${params.toString()}`);
}
```

Modifier `loadFromURL` pour lire `dept` et afficher `commune (dept)` dans le champ :

```js
const dept = params.get("dept") || "";
communeInput.value = dept ? `${commune} (${dept})` : commune;
selectedCommune = {
  nom: commune,
  departement: dept,
  latitude: lat,
  longitude: lon,
};
```

---

## 4) Plan d'implémentation

### Étape 1 — Corrections mineures R6 et R7

**Fichiers** : `public/app.js`

- Remplacer `innerHTML = ""` par `replaceChildren()` (2 occurrences)
- Ajouter `dept` dans `updateURL` et `loadFromURL` / `readValidURLParams`
- Afficher `commune (dept)` dans le champ input au chargement par URL

**Testable** : l'application fonctionne comme avant, URL contient `dept`, le champ affiche le département au chargement par URL.

---

### Étape 2 — Périodes prédéfinies

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

- Ajouter le HTML des boutons preset entre les champs date et le bouton Rechercher
- Ajouter les styles CSS pour `.preset-buttons`
- Implémenter `applyPresetPeriod(days)` dans `app.js`
- Connecter les event listeners des boutons preset
- Gérer l'état disabled (même condition que les dates) et la classe `.active`
- Retirer `.active` sur modification manuelle des champs date

**Testable** : sélectionner une commune → les boutons s'activent → clic sur un bouton → les dates se remplissent correctement → le bouton Rechercher s'active.

---

### Étape 3 — Mode comparaison — Structure et UX

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

- Ajouter le HTML du bouton « Comparer avec une autre ville » et du second champ commune
- Implémenter l'auto-complétion du second champ (réutiliser la logique existante via factorisation minimale)
- Gérer `comparisonMode` et `selectedCommune2`
- Bouton « Comparer » visible dès qu'une première commune est sélectionnée
- Bouton « Annuler la comparaison » pour revenir au mode simple
- Le bouton Rechercher est activé si : commune 1 sélectionnée + dates valides + (mode simple OU commune 2 sélectionnée)

**Testable** : activer le mode, saisir une seconde commune, vérifier que le bouton Rechercher est correctement contrôlé.

---

### Étape 4 — Mode comparaison — Fetch et rendu comparatif

**Fichiers** : `public/app.js`

- Refactorer `fetchWeather` en `fetchWeatherForCommune(commune)` (fonction générique)
- Dans `performSearch` : si `comparisonMode && selectedCommune2`, lancer `Promise.all` de deux fetch
- Implémenter `computeAggregates(dailySummaries)` pour les métriques du résumé comparatif
- Implémenter `renderComparisonSummary(agg1, agg2, nom1, nom2)` — tableau HTML comparatif
- Implémenter `renderComparisonResults(result1, result2)` orchestrant résumé, graphique et détails
- Rendu graphique : appeler `renderChart` avec 4 datasets si mode comparaison
- Rendu résumés journaliers : deux blocs avec titre ville + `renderDailySummary` existant
- Rendu accordéon : deux blocs avec titre ville + `renderDayGroups` existant

**Testable** : effectuer une recherche comparative → résumé comparatif visible, graphique avec 4 datasets, détails horaires pour les deux villes.

---

### Étape 5 — URL partageable en mode comparaison

**Fichiers** : `public/app.js`

- Étendre `updateURL` pour écrire `commune2`, `dept2`, `lat2`, `lon2` si mode comparaison
- Étendre `readValidURLParams` pour parser les paramètres de la 2e ville
- Étendre `loadFromURL` pour activer le mode comparaison si les paramètres 2e ville sont présents
- Validation : mêmes contrôles lat/lon range que pour la ville 1

**Testable** : effectuer une recherche comparative → copier l'URL → recharger → la comparaison se relance automatiquement.

---

### Étape 6 — Tests

**Fichiers** : `tests/test_api.py`

Nouveaux tests backend (même endpoint, validation des cas d'usage liés aux appels parallèles) :

| Test                                                          | Assertion                             |
| ------------------------------------------------------------- | ------------------------------------- |
| Deux appels `/api/weather` simultanés avec des communes diff. | Les deux retournent 200 avec des data |

> Le mode comparaison étant purement frontend, les tests backend se limitent à vérifier que l'API supporte des appels concurrents sans interférence (via le cache).

Tests frontend basiques si un framework de test JS est en place (non requis dans l'immédiat — le projet n'a pas de test runner frontend actuellement).

**Testable** : `pytest` passe sans SKIP ni DeprecationWarning projet.

---

## 5) Guide pour le Développeur

### Pièges fréquents

1. **Calcul des dates preset** : Attention aux fuseaux horaires dans `new Date()`. Toujours travailler à partir de la chaîne `maxDateValue` (ISO `YYYY-MM-DD`) et utiliser `.toISOString().slice(0, 10)` pour rester en date pure. Ne pas utiliser `toLocaleDateString` qui dépend du navigateur.

2. **Auto-complétion du 2e champ** : Ne pas dupliquer tout le code de l'auto-complétion. Factoriser les event listeners communs via une fonction `setupCommuneAutocomplete(inputEl, listEl, messageEl, onSelect)` qui retourne les mêmes handlers debounce + keyboard. Le premier champ existant peut être refactoré pour utiliser cette même fonction.

3. **`Promise.all` failure mode** : Si une des deux requêtes échoue, `Promise.all` rejette immédiatement. Le `catch` dans `performSearch` gère déjà ce cas (message d'erreur global). Ne pas essayer de faire de rendu partiel — c'est tout ou rien.

4. **État du bouton Rechercher** : La condition doit devenir :

   ```
   disabled = !(selectedCommune && isDateRangeValid() && (!comparisonMode || selectedCommune2))
   ```

   Ne pas oublier de rappeler `updateSearchButtonState()` lors du toggle du mode comparaison.

5. **Nettoyage à la désactivation du mode comparaison** : Quand l'utilisateur clique « Annuler la comparaison » :
   - `comparisonMode = false`
   - `selectedCommune2 = null`
   - Vider le champ `commune2-input`
   - Masquer `#compare-fields`
   - Masquer `#comparison-summary`
   - Si des résultats de comparaison sont affichés, les nettoyer et ré-afficher les résultats simple ville (ou masquer la section résultats)
   - Mettre à jour l'URL (supprimer `commune2`, `dept2`, `lat2`, `lon2`)
   - Rappeler `updateSearchButtonState()`

### Zones de dérive

- **Ne pas ajouter de 3e ville** — le scope est strictement 2 communes max.
- **Ne pas créer un endpoint backend `/api/compare`** — le frontend orchestre les deux appels.
- **Ne pas ajouter de persistance** (localStorage, cookies) pour mémoriser les villes récentes ou comparaisons favorites — INV-1.
- **Ne pas ajouter un mode comparaison « avancé »** avec des métriques supplémentaires non demandées (UV, pression, etc.) — les indicateurs sont ceux déjà retournés par l'API.

### Simplifications autorisées

- La factorisation de l'auto-complétion peut rester minimaliste : si le coût de refactoring est trop élevé, il est acceptable de dupliquer le code de gestion du 2e champ tant que le comportement est identique.
- Le résumé comparatif peut être un simple tableau HTML plutôt que des cartes stylées.
- En mobile, les deux blocs de résultats (ville 1 / ville 2) peuvent s'empiler verticalement sans layout côte à côte — c'est attendu et suffisant.

### Décisions explicitement interdites

- Modifier la structure de la réponse de `/api/weather`
- Modifier `src/weather_service.py` ou `src/commune_service.py`
- Ajouter un routeur frontend
- Introduire un state manager (Redux, MobX, etc.)
- Ajouter un bundler ou un build step

---

## 6) Stratégie de tests

### Tests unitaires backend

Les tests existants (26) restent inchangés et doivent continuer à passer.

Nouveau test :

| Fichier       | Test                               | Description                                                             |
| ------------- | ---------------------------------- | ----------------------------------------------------------------------- |
| `test_api.py` | `test_concurrent_weather_requests` | Deux appels simultanés `/api/weather` avec des coords différentes → 200 |

### Tests manuels frontend (checklist)

| #   | Scénario                                                | Résultat attendu                                                    |
| --- | ------------------------------------------------------- | ------------------------------------------------------------------- |
| M1  | Clic « 7 jours » sans commune sélectionnée              | Bouton désactivé, rien ne se passe                                  |
| M2  | Sélectionner commune → clic « Hier »                    | date-start = date-end = veille, bouton Rechercher actif             |
| M3  | Clic « 30 jours »                                       | date-start = veille −29, date-end = veille                          |
| M4  | Clic « 30 jours » puis modifier date-start manuellement | Classe `.active` retirée du bouton 30 jours                         |
| M5  | Activer mode comparaison → vérifier 2e champ visible    | Auto-complétion fonctionnelle, bouton Rechercher désactivé          |
| M6  | Sélectionner 2 communes + dates → Rechercher            | Résumé comparatif, graphique 4 datasets, 2 blocs horaires           |
| M7  | Annuler la comparaison                                  | Retour mode simple, 2e champ masqué, résultats comparatifs nettoyés |
| M8  | URL avec params comparaison → recharger                 | Mode comparaison activé, recherche auto                             |
| M9  | URL avec params simples + dept → recharger              | Champ affiche `Nom (dept)`                                          |
| M10 | Mode comparaison + erreur API sur 1 des 2 villes        | Message d'erreur global, pas de rendu partiel                       |
| M11 | Responsive 360px en mode comparaison                    | Empilement vertical, pas de débordement horizontal                  |

### Edge cases

- Deux fois la même commune en comparaison → autorisé, affiche les mêmes données (pas de validation à ajouter, serait de la sur-ingénierie)
- Période de 1 jour en comparaison → fonctionnel, accordéon ouvert pour les deux villes
- Chargement URL avec `commune2` mais sans `lat2`/`lon2` → mode simple (paramètres comparaison ignorés)

---

## 7) Risques techniques

| #   | Risque                                                                                                        | Mitigation                                                                                                                                |
| --- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| 1   | **Double appel API** → latence perçue doublée si les deux requêtes sont séquentielles                         | `Promise.all` assure le parallélisme. Afficher un indicateur de chargement (le bouton « Chargement... » existant suffit).                 |
| 2   | **Complexité du state frontend** — `comparisonMode`, `selectedCommune2` augmentent les combinaisons possibles | Limiter les branches : mode simple = code existant inchangé. Le mode comparaison est un `if` additionnel dans `performSearch` uniquement. |
| 3   | **Graphique Chart.js avec 4 datasets** — lisibilité réduite sur petits écrans                                 | Différencier visuellement (trait plein vs pointillé, couleurs contrastées). En mobile, le graphique reste scrollable.                     |
