# 001 — HistoMeteo MVP — Spec Technique v20

> **Source fonctionnelle** : `001-histometeo-mvp.md`
> **Base technique** : `001-histometeo-mvp.tech.v19.md`
> **Feedback intégré** : `feedback-to-architect-001-v19.md` (R58, R59, R60, R61, R62)
> **Date** : 2026-03-13

---

## 0) Contract

- **Source of truth** : cette spec technique (`001-histometeo-mvp.tech.v20.md`)
- **Functional integrity** : AC1–AC30 inchangés. Aucun nouveau critère d'acceptation.
- **Scope** — fichier modifiable :
  - `public/app.js` (correction R58 + R59)
  - `public/index.html` (R60)
  - `tests/test_prefetch_service.py` (R61)
- **Forbidden changes** :
  - Tous les fichiers non listés dans le Scope ci-dessus
  - Tests existants ≤ v19 (109 tests) — ne pas modifier, tous doivent continuer à passer
- **Invariants** :
  - INV-1 à INV-33 : tous préservés (cf. v19)
- **Done when** :
  - D33 : `node -c public/app.js` s'exécute sans erreur de syntaxe
  - D34 : Le bloc `if (seoState.mode === "comparison")` dans `loadFromURL()` est correctement fermé avec `}`
  - D35 : Les lignes à l'intérieur du bloc comparison sont uniformément indentées à 8 espaces
  - D36 : Le bouton `#retry-button` dans `index.html` porte la classe `btn-primary`
  - D37 : Un test vérifie la non-collision de clés de cache pour quelques slugs représentatifs
  - D38 : 109 tests hérités passent + nouveau(x) test(s) R61
  - D39 : Les fonctionnalités AC24–AC30 sont fonctionnelles dans le navigateur (le JavaScript s'exécute correctement)

---

## 1) Objectif technique

Corriger l'erreur de syntaxe JavaScript bloquante introduite en v19 dans `loadFromURL()`, normaliser l'indentation du bloc impacté, et intégrer deux améliorations mineures (conformité HTML, couverture de test).

Cette itération ne modifie **aucune logique métier**. Le backend est validé et complet depuis v19.

---

## 2) Analyse du brief

### Besoins principaux

| #   | Besoin                                                            | Origine          |
| --- | ----------------------------------------------------------------- | ---------------- |
| B22 | Corriger l'erreur de syntaxe JS empêchant l'exécution du frontend | Feedback v19 R58 |
| B23 | Normaliser l'indentation du bloc comparison dans `loadFromURL()`  | Feedback v19 R59 |
| B24 | Conformité HTML : classe `btn-primary` sur le bouton Réessayer    | Feedback v19 R60 |
| B25 | Couverture test : vérifier la non-collision des clés de cache     | Feedback v19 R61 |

### Contraintes

- Correction chirurgicale — aucune modification de logique
- Le code backend (routes, prefetch, cache) est correct et NE DOIT PAS être touché
- Les 109 tests existants doivent continuer à passer sans modification

### Risques

Voir §7.

---

## 3) Design minimal proposé

### 3.1 Correction R58 — Accolade fermante manquante dans `loadFromURL()`

**Diagnostic** : dans `public/app.js`, la fonction `loadFromURL()` contient un bloc `if (seoState.mode === "comparison") {` (ligne ~3390) qui n'est jamais fermé. L'accolade `}` fermant ce bloc a été perdue lors du refactoring v19 (extraction du mode "simple" dans son propre bloc `if`). Le `} catch (error) {` ferme le `try` mais le bloc `if (comparison)` est toujours ouvert, ce qui produit une `SyntaxError: Unexpected token 'catch'`.

**Code actuel (cassé)** :

```javascript
      if (seoState.mode === "comparison") {
        const [communeA, communeB] = await Promise.all([
          fetchResolvedCommune(seoState.slug1),
          fetchResolvedCommune(seoState.slug2),
        ]);

        selectedCommune = communeA;
        selectedCommune2 = communeB;
        comparisonMode = true;
        currentPageMode = "comparison";
        currentSlug = "";
        compareFields.classList.remove("hidden");
        compareButton.classList.add("hidden");
        compareButton.disabled = false;
        dateStartController.setDisabled(false);
        dateEndController.setDisabled(false);
        setPresetButtonsDisabled(false);
        communeInput.value = formatCommuneLabel(selectedCommune);
        commune2Input.value = formatCommuneLabel(selectedCommune2);
      dateStart.value = seoState.start;            // ← 6 espaces (devrait être 8)
      dateEnd.value = seoState.end;                // ← 6 espaces (devrait être 8)
      dateStartController.syncHiddenToText();      // ← 6 espaces (devrait être 8)
      dateEndController.syncHiddenToText();        // ← 6 espaces (devrait être 8)
                                                   //
      updateCanonicalLink(window.location.pathname);
      updateSearchButtonState();
                                                   //
      if (isDateRangeValid()) {                    // ← 6 espaces (devrait être 8)
        await performSearch();
      }
      return;
    } catch (error) {  // ← SyntaxError ici : le bloc comparison n'est pas fermé
```

**Code corrigé** :

```javascript
      if (seoState.mode === "comparison") {
        const [communeA, communeB] = await Promise.all([
          fetchResolvedCommune(seoState.slug1),
          fetchResolvedCommune(seoState.slug2),
        ]);

        selectedCommune = communeA;
        selectedCommune2 = communeB;
        comparisonMode = true;
        currentPageMode = "comparison";
        currentSlug = "";
        compareFields.classList.remove("hidden");
        compareButton.classList.add("hidden");
        compareButton.disabled = false;
        dateStartController.setDisabled(false);
        dateEndController.setDisabled(false);
        setPresetButtonsDisabled(false);
        communeInput.value = formatCommuneLabel(selectedCommune);
        commune2Input.value = formatCommuneLabel(selectedCommune2);
        dateStart.value = seoState.start;
        dateEnd.value = seoState.end;
        dateStartController.syncHiddenToText();
        dateEndController.syncHiddenToText();

        updateCanonicalLink(window.location.pathname);
        updateSearchButtonState();

        if (isDateRangeValid()) {
          await performSearch();
        }
        return;
      }
    } catch (error) {
```

**Changements exacts** :

1. Lignes `dateStart.value` à `return;` : indentation passée de 6 à 8 espaces (à l'intérieur du bloc `if (comparison)`)
2. Ajout de `}` (8 espaces d'indentation) après `return;` pour fermer le bloc `if (seoState.mode === "comparison")`

> **Choix de l'option B du feedback** : toutes les lignes après `commune2Input.value` appartiennent logiquement au bloc comparison (pas de mode "simple" après). Les regrouper à l'intérieur du `if` est la correction la plus naturelle et cohérente.

### 3.2 Correction R59 — Normalisation de l'indentation

Couvert par §3.1 ci-dessus. Les lignes ~3407–3419 passent de 6 espaces à 8 espaces pour être cohérentes avec le reste du bloc `if (comparison)`.

### 3.3 Correction R60 — Classe `btn-primary` sur le bouton Réessayer

**Dans `public/index.html`**

Avant :

```html
<button type="button" id="retry-button" class="hidden"></button>
```

Après :

```html
<button type="button" id="retry-button" class="btn-primary hidden"></button>
```

Alignement avec la spec v19 §3.5.1.

### 3.4 Correction R61 — Test de non-collision de clés de cache

**Dans `tests/test_prefetch_service.py`**, ajouter un test vérifiant que des slugs proches mais distincts produisent des clés de cache différentes :

```python
def test_cache_key_no_collision():
    """Vérifie que des slugs différents produisent des clés de cache distinctes."""
    keys = {
        cache_key_period("gap-05", "2026-03-01", "2026-03-07"),
        cache_key_period("gap-05", "2026-03-01", "2026-03-08"),
        cache_key_period("gap-06", "2026-03-01", "2026-03-07"),
        cache_key_month("gap-05", 2026, 3),
        cache_key_month("gap-05", 2026, 4),
        cache_key_town("gap-05"),
        cache_key_town("gap-06"),
        cache_key_period("saint-veran-05", "2026-03-01", "2026-03-07"),
        cache_key_period("saint-véran-05", "2026-03-01", "2026-03-07"),
    }
    # Toutes les clés doivent être uniques (set déduplique)
    assert len(keys) == 9
```

Ce test importe directement les fonctions `cache_key_*` depuis `src.prefetch_service` et vérifie l'injectivité sur des cas représentatifs (même commune/dates différentes, dates identiques/communes différentes, caractères accentués, types différents).

---

## 4) Plan d'implémentation

| Étape | Description                                                                                                     | Fichier                          | Testable                                                |
| ----- | --------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------- |
| E1    | Corriger R58 + R59 : ajouter `}` fermant le bloc comparison + reindenter à 8 espaces                            | `public/app.js`                  | `node -c public/app.js` → pas d'erreur                  |
| E2    | Corriger R60 : ajouter `class="btn-primary"` sur `#retry-button`                                                | `public/index.html`              | Inspection HTML                                         |
| E3    | Ajouter le test R61 : `test_cache_key_no_collision`                                                             | `tests/test_prefetch_service.py` | `pytest tests/test_prefetch_service.py -k no_collision` |
| E4    | Validation globale : `pytest tests/ -v` → 109 hérités + 1 nouveau = 110 tests PASSED                            | —                                | Tous les tests passent                                  |
| E5    | Test manuel : ouvrir une page SEO dans le navigateur → le JavaScript s'exécute, les AC24–AC30 sont fonctionnels | —                                | Les fonctionnalités AC24–AC30 sont opérationnelles      |

---

## 5) Guide pour le Développeur

### Pièges fréquents

1. **L'accolade manquante est la SEULE correction dans `app.js`** — ne pas toucher à la logique, aux variables, ni aux appels de fonctions. La correction est syntaxique uniquement (1 accolade + reindentation).

2. **Vérifier la syntaxe après la correction** — exécuter `node -c public/app.js` pour confirmer l'absence d'erreur de syntaxe. C'est le test de validation primaire pour cette itération.

3. **L'indentation du fichier `app.js` utilise 2 espaces comme unité** — les lignes à l'intérieur du bloc comparison sont à 4 niveaux d'imbrication (function → try → if(seoState) → if(comparison)) = 8 espaces.

4. **Ne pas modifier les lignes AVANT `dateStart.value`** — les lignes 3390–3406 (de `if (seoState.mode === "comparison")` à `commune2Input.value`) sont déjà correctement indentées à 8 espaces.

### Zones de dérive

- Ne pas profiter de cette correction pour refactorer `loadFromURL()` — la portée est strictement R58/R59/R60/R61
- Ne pas modifier de fichier backend — le backend est correct et complet
- Ne pas modifier de test existant

### Simplifications autorisées

- Aucune — modification chirurgicale

### Décisions explicitement interdites

- Ne pas modifier la logique du bloc comparison (même si c'est tentant)
- Ne pas réorganiser le flux `loadFromURL()` (hors scope)
- Ne pas toucher au CSS (le style du retry-button est fonctionnel via `#retry-button`)

---

## 6) Stratégie de tests

### Tests existants à préserver

Les 109 tests existants doivent tous passer sans modification.

### Nouveau test

| #   | Test                          | Vérification                                                                                   |
| --- | ----------------------------- | ---------------------------------------------------------------------------------------------- |
| T17 | `test_cache_key_no_collision` | 9 clés générées à partir de slugs/dates proches mais distincts → toutes uniques (set size = 9) |

### Vérification syntaxique (requise)

```bash
node -c public/app.js
```

Doit retourner un exit code 0 sans sortie.

### Tests manuels

| #   | Scénario                                                            | Vérification                                                                                                             |
| --- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| M41 | Ouvrir une page SEO `/meteo/gap-05/2026-03-01/2026-03-07`           | Les données s'affichent. Le JavaScript s'exécute (pas de squelette HTML vide). Vérifier la console : aucune SyntaxError. |
| M42 | Ouvrir une page de comparaison `/comparaison/gap-05/vs/lyon-69/...` | La comparaison fonctionne (flux API classique). Pas de régression.                                                       |
| M43 | Simuler erreur API → message d'erreur + bouton Réessayer visible    | Le bouton Réessayer a le style `btn-primary` (fond accent). Cliquer → relance la requête.                                |
| M44 | Vérifier dans DevTools : aucune erreur dans la console JS           | Pas de `SyntaxError`, pas de `ReferenceError`, pas de `TypeError`.                                                       |

### Total tests attendu

109 (hérités) + 1 nouveau = **110 tests minimum**

---

## 7) Risques techniques

| #   | Risque                                                                   | Probabilité | Mitigation                                                                                                                                 |
| --- | ------------------------------------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| R1  | La correction R58 introduit un autre bug syntaxique                      | Faible      | `node -c public/app.js` vérifie l'intégralité du fichier avant tout test fonctionnel. R62 (CI check) recommandé pour les futures versions. |
| R2  | L'indentation modifiée (R59) crée un diff bruyant qui masque un problème | Très faible | Le diff ne contient que des changements d'espaces en début de ligne + 1 accolade ajoutée. Review visuelle simple.                          |
| R3  | Le test R61 est un faux positif (les clés sont uniques par construction) | Très faible | Le test sert de filet de sécurité pour les refactorings futurs de `cache_key_*`. Coût nul, bénéfice préventif.                             |
