---
name: project_leases_grouping
description: Page Leases groupée par subnet + sous-groupes de réservation, toggle interne, collapse/expand
metadata:
  type: project
---

La page **Leases** (`public/leases.php`) affiche les baux DHCP **toujours groupés par subnet**
(en-tête niveau 1, subnets triés alpha). À l'intérieur d'un subnet, un bouton **Groupés / Plat**
(persisté `localStorage` clé `leases_view_mode`, groupé par défaut) contrôle uniquement
l'affichage interne :
- **Groupés** : sous-groupes par `group_name` (jointure `app_reservation_meta`), sous-en-tête
  `.group-subheader` indenté, `(sans groupe)` en dernier. Comme `subnet.php`.
- **Plat** : liste simple des leases du subnet.

**Collapse/expand** : en-têtes subnet ET sous-groupe cliquables (caret ▾/▸). L'état de repli
vit dans des `Set` JS (`collapsedSubnets`, `collapsedGroups`) — **pas dans le DOM** — pour
survivre aux re-renders (filtre, tri, refresh auto 30s, car la page se rend en innerHTML).
Un en-tête replié n'émet pas ses lignes filles. Handler par délégation sur le `<tbody>`.
Groupe sans nom → jeton d'état `__none__` (évite l'ambiguïté d'un `data-group` à valeur d'espace).

**Piège récurrent de cette page** : rendu 100% JS via innerHTML reconstruit à chaque `render()`
→ tout état d'UI doit être conservé hors DOM. Noms de subnet/groupe **échappés** (escapeHtml)
comme le fix XSS de [[reserved_ips]]. Livré aux commits ba1065a (groupement) + b0778c3 (collapse).

**Vérif sans navigateur** : la logique JS (groupement/tri/repli) est testable en isolation avec
Node (extraire les fonctions pures dans un script scratchpad) ; l'API Kea live n'est pas
joignable pour piloter la vraie page → le rendu visuel final reste à valider côté utilisateur.
